diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index 7f9e98f91..23d63c946 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -10,6 +10,7 @@ import ( "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -24,8 +25,9 @@ const ( ) // ListNotifications creates a tool to list notifications for the current user. -func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ListNotifications(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "list_notifications", Description: t("TOOL_LIST_NOTIFICATIONS_DESCRIPTION", "Lists all GitHub notifications for the authenticated user, including unread notifications, mentions, review requests, assignments, and updates on issues or pull requests. Use this tool whenever the user asks what to work on next, requests a summary of their GitHub activity, wants to see pending reviews, or needs to check for new updates or tasks. This tool is the primary way to discover actionable items, reminders, and outstanding work on GitHub. Always call this tool when asked what to work on next, what is pending, or what needs attention in GitHub."), Annotations: &mcp.ToolAnnotations{ @@ -59,106 +61,110 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu }, }), }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - filter, err := OptionalParam[string](args, "filter") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + filter, err := OptionalParam[string](args, "filter") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - since, err := OptionalParam[string](args, "since") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + since, err := OptionalParam[string](args, "since") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - before, err := OptionalParam[string](args, "before") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + before, err := OptionalParam[string](args, "before") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - owner, err := OptionalParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := OptionalParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := OptionalParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := OptionalParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - paginationParams, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + paginationParams, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - // Build options - opts := &github.NotificationListOptions{ - All: filter == FilterIncludeRead, - Participating: filter == FilterOnlyParticipating, - ListOptions: github.ListOptions{ - Page: paginationParams.Page, - PerPage: paginationParams.PerPage, - }, - } + // Build options + opts := &github.NotificationListOptions{ + All: filter == FilterIncludeRead, + Participating: filter == FilterOnlyParticipating, + ListOptions: github.ListOptions{ + Page: paginationParams.Page, + PerPage: paginationParams.PerPage, + }, + } - // Parse time parameters if provided - if since != "" { - sinceTime, err := time.Parse(time.RFC3339, since) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid since time format, should be RFC3339/ISO8601: %v", err)), nil, nil + // Parse time parameters if provided + if since != "" { + sinceTime, err := time.Parse(time.RFC3339, since) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("invalid since time format, should be RFC3339/ISO8601: %v", err)), nil, nil + } + opts.Since = sinceTime } - opts.Since = sinceTime - } - if before != "" { - beforeTime, err := time.Parse(time.RFC3339, before) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid before time format, should be RFC3339/ISO8601: %v", err)), nil, nil + if before != "" { + beforeTime, err := time.Parse(time.RFC3339, before) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("invalid before time format, should be RFC3339/ISO8601: %v", err)), nil, nil + } + opts.Before = beforeTime } - opts.Before = beforeTime - } - var notifications []*github.Notification - var resp *github.Response + var notifications []*github.Notification + var resp *github.Response - if owner != "" && repo != "" { - notifications, resp, err = client.Activity.ListRepositoryNotifications(ctx, owner, repo, opts) - } else { - notifications, resp, err = client.Activity.ListNotifications(ctx, opts) - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list notifications", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if owner != "" && repo != "" { + notifications, resp, err = client.Activity.ListRepositoryNotifications(ctx, owner, repo, opts) + } else { + notifications, resp, err = client.Activity.ListNotifications(ctx, opts) + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list notifications", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to get notifications: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + // Marshal response to JSON + r, err := json.Marshal(notifications) if err != nil { - return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to get notifications: %s", string(body))), nil, nil - } - // Marshal response to JSON - r, err := json.Marshal(notifications) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, err + return utils.NewToolResultText(string(r)), nil, nil } - - return utils.NewToolResultText(string(r)), nil, nil - }) + }, + ) } // DismissNotification creates a tool to mark a notification as read/done. -func DismissNotification(getclient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func DismissNotification(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "dismiss_notification", Description: t("TOOL_DISMISS_NOTIFICATION_DESCRIPTION", "Dismiss a notification by marking it as read or done"), Annotations: &mcp.ToolAnnotations{ @@ -181,62 +187,66 @@ func DismissNotification(getclient GetClientFn, t translations.TranslationHelper Required: []string{"threadID", "state"}, }, }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getclient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } - - threadID, err := RequiredParam[string](args, "threadID") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - state, err := RequiredParam[string](args, "state") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + threadID, err := RequiredParam[string](args, "threadID") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - switch state { - case "done": - // for some inexplicable reason, the API seems to have threadID as int64 and string depending on the endpoint - var threadIDInt int64 - threadIDInt, err = strconv.ParseInt(threadID, 10, 64) + state, err := RequiredParam[string](args, "state") if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid threadID format: %v", err)), nil, nil + return utils.NewToolResultError(err.Error()), nil, nil } - resp, err = client.Activity.MarkThreadDone(ctx, threadIDInt) - case "read": - resp, err = client.Activity.MarkThreadRead(ctx, threadID) - default: - return utils.NewToolResultError("Invalid state. Must be one of: read, done."), nil, nil - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to mark notification as %s", state), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + var resp *github.Response + switch state { + case "done": + // for some inexplicable reason, the API seems to have threadID as int64 and string depending on the endpoint + var threadIDInt int64 + threadIDInt, err = strconv.ParseInt(threadID, 10, 64) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("invalid threadID format: %v", err)), nil, nil + } + resp, err = client.Activity.MarkThreadDone(ctx, threadIDInt) + case "read": + resp, err = client.Activity.MarkThreadRead(ctx, threadID) + default: + return utils.NewToolResultError("Invalid state. Must be one of: read, done."), nil, nil + } - if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) if err != nil { - return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to mark notification as %s", state), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to mark notification as %s: %s", state, string(body))), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to mark notification as %s: %s", state, string(body))), nil, nil - } - return utils.NewToolResultText(fmt.Sprintf("Notification marked as %s", state)), nil, nil - }) + return utils.NewToolResultText(fmt.Sprintf("Notification marked as %s", state)), nil, nil + } + }, + ) } // MarkAllNotificationsRead creates a tool to mark all notifications as read. -func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func MarkAllNotificationsRead(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "mark_all_notifications_read", Description: t("TOOL_MARK_ALL_NOTIFICATIONS_READ_DESCRIPTION", "Mark all notifications as read"), Annotations: &mcp.ToolAnnotations{ @@ -261,70 +271,74 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH }, }, }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } - - lastReadAt, err := OptionalParam[string](args, "lastReadAt") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - owner, err := OptionalParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := OptionalParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + lastReadAt, err := OptionalParam[string](args, "lastReadAt") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var lastReadTime time.Time - if lastReadAt != "" { - lastReadTime, err = time.Parse(time.RFC3339, lastReadAt) + owner, err := OptionalParam[string](args, "owner") if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid lastReadAt time format, should be RFC3339/ISO8601: %v", err)), nil, nil + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := OptionalParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil } - } else { - lastReadTime = time.Now() - } - markReadOptions := github.Timestamp{ - Time: lastReadTime, - } + var lastReadTime time.Time + if lastReadAt != "" { + lastReadTime, err = time.Parse(time.RFC3339, lastReadAt) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("invalid lastReadAt time format, should be RFC3339/ISO8601: %v", err)), nil, nil + } + } else { + lastReadTime = time.Now() + } - var resp *github.Response - if owner != "" && repo != "" { - resp, err = client.Activity.MarkRepositoryNotificationsRead(ctx, owner, repo, markReadOptions) - } else { - resp, err = client.Activity.MarkNotificationsRead(ctx, markReadOptions) - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to mark all notifications as read", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + markReadOptions := github.Timestamp{ + Time: lastReadTime, + } - if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + var resp *github.Response + if owner != "" && repo != "" { + resp, err = client.Activity.MarkRepositoryNotificationsRead(ctx, owner, repo, markReadOptions) + } else { + resp, err = client.Activity.MarkNotificationsRead(ctx, markReadOptions) + } if err != nil { - return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to mark all notifications as read", + resp, + err, + ), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to mark all notifications as read: %s", string(body))), nil, nil - } + defer func() { _ = resp.Body.Close() }() - return utils.NewToolResultText("All notifications marked as read"), nil, nil - }) + if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to mark all notifications as read: %s", string(body))), nil, nil + } + + return utils.NewToolResultText("All notifications marked as read"), nil, nil + } + }, + ) } // GetNotificationDetails creates a tool to get details for a specific notification. -func GetNotificationDetails(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func GetNotificationDetails(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "get_notification_details", Description: t("TOOL_GET_NOTIFICATION_DETAILS_DESCRIPTION", "Get detailed information for a specific GitHub notification, always call this tool when the user asks for details about a specific notification, if you don't know the ID list notifications first."), Annotations: &mcp.ToolAnnotations{ @@ -342,42 +356,45 @@ func GetNotificationDetails(getClient GetClientFn, t translations.TranslationHel Required: []string{"notificationID"}, }, }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - notificationID, err := RequiredParam[string](args, "notificationID") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + notificationID, err := RequiredParam[string](args, "notificationID") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - thread, resp, err := client.Activity.GetThread(ctx, notificationID) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to get notification details for ID '%s'", notificationID), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + thread, resp, err := client.Activity.GetThread(ctx, notificationID) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get notification details for ID '%s'", notificationID), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("failed to get notification details: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + r, err := json.Marshal(thread) if err != nil { - return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, err + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to get notification details: %s", string(body))), nil, nil - } - r, err := json.Marshal(thread) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, err + return utils.NewToolResultText(string(r)), nil, nil } - - return utils.NewToolResultText(string(r)), nil, nil - }) + }, + ) } // Enum values for ManageNotificationSubscription action @@ -388,8 +405,9 @@ const ( ) // ManageNotificationSubscription creates a tool to manage a notification subscription (ignore, watch, delete) -func ManageNotificationSubscription(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ManageNotificationSubscription(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "manage_notification_subscription", Description: t("TOOL_MANAGE_NOTIFICATION_SUBSCRIPTION_DESCRIPTION", "Manage a notification subscription: ignore, watch, or delete a notification thread subscription."), Annotations: &mcp.ToolAnnotations{ @@ -412,65 +430,68 @@ func ManageNotificationSubscription(getClient GetClientFn, t translations.Transl Required: []string{"notificationID", "action"}, }, }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - notificationID, err := RequiredParam[string](args, "notificationID") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - action, err := RequiredParam[string](args, "action") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + notificationID, err := RequiredParam[string](args, "notificationID") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + action, err := RequiredParam[string](args, "action") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var ( - resp *github.Response - result any - apiErr error - ) - - switch action { - case NotificationActionIgnore: - sub := &github.Subscription{Ignored: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub) - case NotificationActionWatch: - sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub) - case NotificationActionDelete: - resp, apiErr = client.Activity.DeleteThreadSubscription(ctx, notificationID) - default: - return utils.NewToolResultError("Invalid action. Must be one of: ignore, watch, delete."), nil, nil - } + var ( + resp *github.Response + result any + apiErr error + ) + + switch action { + case NotificationActionIgnore: + sub := &github.Subscription{Ignored: ToBoolPtr(true)} + result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub) + case NotificationActionWatch: + sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} + result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub) + case NotificationActionDelete: + resp, apiErr = client.Activity.DeleteThreadSubscription(ctx, notificationID) + default: + return utils.NewToolResultError("Invalid action. Must be one of: ignore, watch, delete."), nil, nil + } - if apiErr != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to %s notification subscription", action), - resp, - apiErr, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if apiErr != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to %s notification subscription", action), + resp, + apiErr, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - return utils.NewToolResultError(fmt.Sprintf("failed to %s notification subscription: %s", action, string(body))), nil, nil - } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return utils.NewToolResultError(fmt.Sprintf("failed to %s notification subscription: %s", action, string(body))), nil, nil + } - if action == NotificationActionDelete { - // Special case for delete as there is no response body - return utils.NewToolResultText("Notification subscription deleted"), nil, nil - } + if action == NotificationActionDelete { + // Special case for delete as there is no response body + return utils.NewToolResultText("Notification subscription deleted"), nil, nil + } - r, err := json.Marshal(result) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, err + r, err := json.Marshal(result) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + }, + ) } const ( @@ -480,8 +501,9 @@ const ( ) // ManageRepositoryNotificationSubscription creates a tool to manage a repository notification subscription (ignore, watch, delete) -func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ManageRepositoryNotificationSubscription(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "manage_repository_notification_subscription", Description: t("TOOL_MANAGE_REPOSITORY_NOTIFICATION_SUBSCRIPTION_DESCRIPTION", "Manage a repository notification subscription: ignore, watch, or delete repository notifications subscription for the provided repository."), Annotations: &mcp.ToolAnnotations{ @@ -508,70 +530,73 @@ func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translati Required: []string{"owner", "repo", "action"}, }, }, - mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, err - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - action, err := RequiredParam[string](args, "action") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + action, err := RequiredParam[string](args, "action") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var ( - resp *github.Response - result any - apiErr error - ) - - switch action { - case RepositorySubscriptionActionIgnore: - sub := &github.Subscription{Ignored: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) - case RepositorySubscriptionActionWatch: - sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) - case RepositorySubscriptionActionDelete: - resp, apiErr = client.Activity.DeleteRepositorySubscription(ctx, owner, repo) - default: - return utils.NewToolResultError("Invalid action. Must be one of: ignore, watch, delete."), nil, nil - } + var ( + resp *github.Response + result any + apiErr error + ) + + switch action { + case RepositorySubscriptionActionIgnore: + sub := &github.Subscription{Ignored: ToBoolPtr(true)} + result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) + case RepositorySubscriptionActionWatch: + sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} + result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) + case RepositorySubscriptionActionDelete: + resp, apiErr = client.Activity.DeleteRepositorySubscription(ctx, owner, repo) + default: + return utils.NewToolResultError("Invalid action. Must be one of: ignore, watch, delete."), nil, nil + } - if apiErr != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to %s repository subscription", action), - resp, - apiErr, - ), nil, nil - } - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } + if apiErr != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to %s repository subscription", action), + resp, + apiErr, + ), nil, nil + } + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } - // Handle non-2xx status codes - if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { - body, _ := io.ReadAll(resp.Body) - return utils.NewToolResultError(fmt.Sprintf("failed to %s repository subscription: %s", action, string(body))), nil, nil - } + // Handle non-2xx status codes + if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { + body, _ := io.ReadAll(resp.Body) + return utils.NewToolResultError(fmt.Sprintf("failed to %s repository subscription: %s", action, string(body))), nil, nil + } - if action == RepositorySubscriptionActionDelete { - // Special case for delete as there is no response body - return utils.NewToolResultText("Repository subscription deleted"), nil, nil - } + if action == RepositorySubscriptionActionDelete { + // Special case for delete as there is no response body + return utils.NewToolResultText("Repository subscription deleted"), nil, nil + } - r, err := json.Marshal(result) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, err + r, err := json.Marshal(result) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - }) + }, + ) } diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go index 37135bf5c..0a330c316 100644 --- a/pkg/github/notifications_test.go +++ b/pkg/github/notifications_test.go @@ -17,8 +17,8 @@ import ( func Test_ListNotifications(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := ListNotifications(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListNotifications(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_notifications", tool.Name) @@ -125,12 +125,15 @@ func Test_ListNotifications(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := ListNotifications(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { - require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { @@ -139,7 +142,6 @@ func Test_ListNotifications(t *testing.T) { return } - require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) t.Logf("textContent: %s", textContent.Text) @@ -154,8 +156,8 @@ func Test_ListNotifications(t *testing.T) { func Test_ManageNotificationSubscription(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := ManageNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ManageNotificationSubscription(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "manage_notification_subscription", tool.Name) @@ -256,10 +258,14 @@ func Test_ManageNotificationSubscription(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := ManageNotificationSubscription(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { require.NoError(t, err) require.NotNil(t, result) @@ -295,8 +301,8 @@ func Test_ManageNotificationSubscription(t *testing.T) { func Test_ManageRepositoryNotificationSubscription(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := ManageRepositoryNotificationSubscription(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ManageRepositoryNotificationSubscription(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "manage_repository_notification_subscription", tool.Name) @@ -415,12 +421,15 @@ func Test_ManageRepositoryNotificationSubscription(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := ManageRepositoryNotificationSubscription(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { - require.NoError(t, err) require.NotNil(t, result) text := getTextResult(t, result).Text switch { @@ -461,8 +470,8 @@ func Test_ManageRepositoryNotificationSubscription(t *testing.T) { func Test_DismissNotification(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := DismissNotification(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := DismissNotification(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "dismiss_notification", tool.Name) @@ -554,13 +563,16 @@ func Test_DismissNotification(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := DismissNotification(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { // The tool returns a ToolResultError with a specific message - require.NoError(t, err) require.NotNil(t, result) text := getTextResult(t, result).Text switch { @@ -596,8 +608,8 @@ func Test_DismissNotification(t *testing.T) { func Test_MarkAllNotificationsRead(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := MarkAllNotificationsRead(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := MarkAllNotificationsRead(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "mark_all_notifications_read", tool.Name) @@ -676,12 +688,15 @@ func Test_MarkAllNotificationsRead(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := MarkAllNotificationsRead(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { - require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { @@ -702,8 +717,8 @@ func Test_MarkAllNotificationsRead(t *testing.T) { func Test_GetNotificationDetails(t *testing.T) { // Verify tool definition and schema - mockClient := github.NewClient(nil) - tool, _ := GetNotificationDetails(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetNotificationDetails(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_notification_details", tool.Name) @@ -757,12 +772,15 @@ func Test_GetNotificationDetails(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := GetNotificationDetails(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) + require.NoError(t, err) if tc.expectError { - require.NoError(t, err) require.True(t, result.IsError) errorContent := getErrorResult(t, result) if tc.expectedErrMsg != "" { diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index ff81484f2..de5aaea5e 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -11,6 +11,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/raw" + "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -18,834 +19,841 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_commit", - Description: t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "sha": { - Type: "string", - Description: "Commit SHA, branch name, or tag name", - }, - "include_diff": { - Type: "boolean", - Description: "Whether to include file diffs and stats in the response. Default is true.", - Default: json.RawMessage(`true`), - }, +func GetCommit(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "get_commit", + Description: t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"), + ReadOnlyHint: true, }, - Required: []string{"owner", "repo", "sha"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - sha, err := RequiredParam[string](args, "sha") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - includeDiff, err := OptionalBoolParamWithDefault(args, "include_diff", true) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, - } + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "sha": { + Type: "string", + Description: "Commit SHA, branch name, or tag name", + }, + "include_diff": { + Type: "boolean", + Description: "Whether to include file diffs and stats in the response. Default is true.", + Default: json.RawMessage(`true`), + }, + }, + Required: []string{"owner", "repo", "sha"}, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + sha, err := RequiredParam[string](args, "sha") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + includeDiff, err := OptionalBoolParamWithDefault(args, "include_diff", true) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to get commit: %s", sha), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + opts := &github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + } - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get commit: %s", sha), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Convert to minimal commit - minimalCommit := convertToMinimalCommit(commit, includeDiff) + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil + } - r, err := json.Marshal(minimalCommit) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Convert to minimal commit + minimalCommit := convertToMinimalCommit(commit, includeDiff) - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalCommit) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // ListCommits creates a tool to get commits of a branch in a repository. -func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "list_commits", - Description: t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100)."), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "sha": { - Type: "string", - Description: "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", - }, - "author": { - Type: "string", - Description: "Author username or email address to filter commits by", - }, +func ListCommits(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "list_commits", + Description: t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100)."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"), + ReadOnlyHint: true, }, - Required: []string{"owner", "repo"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - sha, err := OptionalParam[string](args, "sha") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - author, err := OptionalParam[string](args, "author") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - // Set default perPage to 30 if not provided - perPage := pagination.PerPage - if perPage == 0 { - perPage = 30 - } - opts := &github.CommitsListOptions{ - SHA: sha, - Author: author, - ListOptions: github.ListOptions{ - Page: pagination.Page, - PerPage: perPage, - }, - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to list commits: %s", sha), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "sha": { + Type: "string", + Description: "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + }, + "author": { + Type: "string", + Description: "Author username or email address to filter commits by", + }, + }, + Required: []string{"owner", "repo"}, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + sha, err := OptionalParam[string](args, "sha") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + author, err := OptionalParam[string](args, "author") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + // Set default perPage to 30 if not provided + perPage := pagination.PerPage + if perPage == 0 { + perPage = 30 + } + opts := &github.CommitsListOptions{ + SHA: sha, + Author: author, + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: perPage, + }, + } - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list commits: %s", string(body))), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list commits: %s", sha), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Convert to minimal commits - minimalCommits := make([]MinimalCommit, len(commits)) - for i, commit := range commits { - minimalCommits[i] = convertToMinimalCommit(commit, false) - } + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list commits: %s", string(body))), nil, nil + } - r, err := json.Marshal(minimalCommits) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Convert to minimal commits + minimalCommits := make([]MinimalCommit, len(commits)) + for i, commit := range commits { + minimalCommits[i] = convertToMinimalCommit(commit, false) + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalCommits) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // ListBranches creates a tool to list branches in a GitHub repository. -func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "list_branches", - Description: t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, +func ListBranches(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "list_branches", + Description: t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"), + ReadOnlyHint: true, }, - Required: []string{"owner", "repo"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.BranchListOptions{ - ListOptions: github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, - }, - } + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + }, + Required: []string{"owner", "repo"}, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + opts := &github.BranchListOptions{ + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + }, + } - branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list branches", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil, nil - } + branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list branches", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Convert to minimal branches - minimalBranches := make([]MinimalBranch, 0, len(branches)) - for _, branch := range branches { - minimalBranches = append(minimalBranches, convertToMinimalBranch(branch)) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil, nil + } - r, err := json.Marshal(minimalBranches) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Convert to minimal branches + minimalBranches := make([]MinimalBranch, 0, len(branches)) + for _, branch := range branches { + minimalBranches = append(minimalBranches, convertToMinimalBranch(branch)) + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalBranches) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository. -func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "create_or_update_file", - Description: t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations."), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner (username or organization)", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "path": { - Type: "string", - Description: "Path where to create/update the file", - }, - "content": { - Type: "string", - Description: "Content of the file", - }, - "message": { - Type: "string", - Description: "Commit message", - }, - "branch": { - Type: "string", - Description: "Branch to create/update the file in", - }, - "sha": { - Type: "string", - Description: "Required if updating an existing file. The blob SHA of the file being replaced.", +func CreateOrUpdateFile(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "create_or_update_file", + Description: t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "path": { + Type: "string", + Description: "Path where to create/update the file", + }, + "content": { + Type: "string", + Description: "Content of the file", + }, + "message": { + Type: "string", + Description: "Commit message", + }, + "branch": { + Type: "string", + Description: "Branch to create/update the file in", + }, + "sha": { + Type: "string", + Description: "Required if updating an existing file. The blob SHA of the file being replaced.", + }, }, + Required: []string{"owner", "repo", "path", "content", "message", "branch"}, }, - Required: []string{"owner", "repo", "path", "content", "message", "branch"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - path, err := RequiredParam[string](args, "path") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - content, err := RequiredParam[string](args, "content") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - message, err := RequiredParam[string](args, "message") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - branch, err := RequiredParam[string](args, "branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // json.Marshal encodes byte arrays with base64, which is required for the API. - contentBytes := []byte(content) + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + path, err := RequiredParam[string](args, "path") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + content, err := RequiredParam[string](args, "content") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + message, err := RequiredParam[string](args, "message") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + branch, err := RequiredParam[string](args, "branch") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - // Create the file options - opts := &github.RepositoryContentFileOptions{ - Message: github.Ptr(message), - Content: contentBytes, - Branch: github.Ptr(branch), - } + // json.Marshal encodes byte arrays with base64, which is required for the API. + contentBytes := []byte(content) - // If SHA is provided, set it (for updates) - sha, err := OptionalParam[string](args, "sha") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if sha != "" { - opts.SHA = github.Ptr(sha) - } + // Create the file options + opts := &github.RepositoryContentFileOptions{ + Message: github.Ptr(message), + Content: contentBytes, + Branch: github.Ptr(branch), + } - // Create or update the file - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + // If SHA is provided, set it (for updates) + sha, err := OptionalParam[string](args, "sha") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if sha != "" { + opts.SHA = github.Ptr(sha) + } - path = strings.TrimPrefix(path, "/") - fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create/update file", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Create or update the file + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != 200 && resp.StatusCode != 201 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to create/update file: %s", string(body))), nil, nil - } + path = strings.TrimPrefix(path, "/") + fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create/update file", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(fileContent) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != 200 && resp.StatusCode != 201 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to create/update file: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(fileContent) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // CreateRepository creates a tool to create a new GitHub repository. -func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "create_repository", - Description: t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account or specified organization"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_CREATE_REPOSITORY_USER_TITLE", "Create repository"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "name": { - Type: "string", - Description: "Repository name", - }, - "description": { - Type: "string", - Description: "Repository description", - }, - "organization": { - Type: "string", - Description: "Organization to create the repository in (omit to create in your personal account)", - }, - "private": { - Type: "boolean", - Description: "Whether repo should be private", - }, - "autoInit": { - Type: "boolean", - Description: "Initialize with README", +func CreateRepository(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "create_repository", + Description: t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account or specified organization"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_REPOSITORY_USER_TITLE", "Create repository"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "name": { + Type: "string", + Description: "Repository name", + }, + "description": { + Type: "string", + Description: "Repository description", + }, + "organization": { + Type: "string", + Description: "Organization to create the repository in (omit to create in your personal account)", + }, + "private": { + Type: "boolean", + Description: "Whether repo should be private", + }, + "autoInit": { + Type: "boolean", + Description: "Initialize with README", + }, }, + Required: []string{"name"}, }, - Required: []string{"name"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - name, err := RequiredParam[string](args, "name") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - description, err := OptionalParam[string](args, "description") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - organization, err := OptionalParam[string](args, "organization") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - private, err := OptionalParam[bool](args, "private") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - autoInit, err := OptionalParam[bool](args, "autoInit") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - repo := &github.Repository{ - Name: github.Ptr(name), - Description: github.Ptr(description), - Private: github.Ptr(private), - AutoInit: github.Ptr(autoInit), - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + description, err := OptionalParam[string](args, "description") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + organization, err := OptionalParam[string](args, "organization") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + private, err := OptionalParam[bool](args, "private") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + autoInit, err := OptionalParam[bool](args, "autoInit") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - createdRepo, resp, err := client.Repositories.Create(ctx, organization, repo) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create repository", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + repo := &github.Repository{ + Name: github.Ptr(name), + Description: github.Ptr(description), + Private: github.Ptr(private), + AutoInit: github.Ptr(autoInit), + } - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to create repository: %s", string(body))), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + createdRepo, resp, err := client.Repositories.Create(ctx, organization, repo) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create repository", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Return minimal response with just essential information - minimalResponse := MinimalResponse{ - ID: fmt.Sprintf("%d", createdRepo.GetID()), - URL: createdRepo.GetHTMLURL(), - } + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to create repository: %s", string(body))), nil, nil + } - r, err := json.Marshal(minimalResponse) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Return minimal response with just essential information + minimalResponse := MinimalResponse{ + ID: fmt.Sprintf("%d", createdRepo.GetID()), + URL: createdRepo.GetHTMLURL(), + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalResponse) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository. -func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_file_contents", - Description: t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner (username or organization)", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "path": { - Type: "string", - Description: "Path to file/directory (directories must end with a slash '/')", - Default: json.RawMessage(`"/"`), - }, - "ref": { - Type: "string", - Description: "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", - }, - "sha": { - Type: "string", - Description: "Accepts optional commit SHA. If specified, it will be used instead of ref", +func GetFileContents(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "get_file_contents", + Description: t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "path": { + Type: "string", + Description: "Path to file/directory (directories must end with a slash '/')", + Default: json.RawMessage(`"/"`), + }, + "ref": { + Type: "string", + Description: "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + }, + "sha": { + Type: "string", + Description: "Accepts optional commit SHA. If specified, it will be used instead of ref", + }, }, + Required: []string{"owner", "repo"}, }, - Required: []string{"owner", "repo"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - path, err := RequiredParam[string](args, "path") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ref, err := OptionalParam[string](args, "ref") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - sha, err := OptionalParam[string](args, "sha") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError("failed to get GitHub client"), nil, nil - } - - rawOpts, err := resolveGitReference(ctx, client, owner, repo, ref, sha) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil - } - - // If the path is (most likely) not to be a directory, we will - // first try to get the raw content from the GitHub raw content API. - - var rawAPIResponseCode int - if path != "" && !strings.HasSuffix(path, "/") { - // First, get file info from Contents API to retrieve SHA - var fileSHA string - opts := &github.RepositoryContentGetOptions{Ref: ref} - fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) - if respContents != nil { - defer func() { _ = respContents.Body.Close() }() - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get file SHA", - respContents, - err, - ), nil, nil - } - if fileContent == nil || fileContent.SHA == nil { - return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil - } - fileSHA = *fileContent.SHA + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + path, err := RequiredParam[string](args, "path") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ref, err := OptionalParam[string](args, "ref") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + sha, err := OptionalParam[string](args, "sha") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - rawClient, err := getRawClient(ctx) - if err != nil { - return utils.NewToolResultError("failed to get GitHub raw content client"), nil, nil - } - resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts) - if err != nil { - return utils.NewToolResultError("failed to get raw repository content"), nil, nil - } - defer func() { - _ = resp.Body.Close() - }() + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError("failed to get GitHub client"), nil, nil + } - if resp.StatusCode == http.StatusOK { - // If the raw content is found, return it directly - body, err := io.ReadAll(resp.Body) + rawOpts, err := resolveGitReference(ctx, client, owner, repo, ref, sha) if err != nil { - return utils.NewToolResultError("failed to read response body"), nil, nil + return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil } - contentType := resp.Header.Get("Content-Type") - var resourceURI string - switch { - case sha != "": - resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path) - if err != nil { - return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + // If the path is (most likely) not to be a directory, we will + // first try to get the raw content from the GitHub raw content API. + + var rawAPIResponseCode int + if path != "" && !strings.HasSuffix(path, "/") { + // First, get file info from Contents API to retrieve SHA + var fileSHA string + opts := &github.RepositoryContentGetOptions{Ref: ref} + fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + if respContents != nil { + defer func() { _ = respContents.Body.Close() }() } - case ref != "": - resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path) if err != nil { - return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get file SHA", + respContents, + err, + ), nil, nil } - default: - resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path) - if err != nil { - return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + if fileContent == nil || fileContent.SHA == nil { + return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil } - } + fileSHA = *fileContent.SHA - // Determine if content is text or binary - isTextContent := strings.HasPrefix(contentType, "text/") || - contentType == "application/json" || - contentType == "application/xml" || - strings.HasSuffix(contentType, "+json") || - strings.HasSuffix(contentType, "+xml") - - if isTextContent { - result := &mcp.ResourceContents{ - URI: resourceURI, - Text: string(body), - MIMEType: contentType, + rawClient, err := deps.GetRawClient(ctx) + if err != nil { + return utils.NewToolResultError("failed to get GitHub raw content client"), nil, nil } - // Include SHA in the result metadata - if fileSHA != "" { - return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded text file (SHA: %s)", fileSHA), result), nil, nil + resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts) + if err != nil { + return utils.NewToolResultError("failed to get raw repository content"), nil, nil + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode == http.StatusOK { + // If the raw content is found, return it directly + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultError("failed to read response body"), nil, nil + } + contentType := resp.Header.Get("Content-Type") + + var resourceURI string + switch { + case sha != "": + resourceURI, err = url.JoinPath("repo://", owner, repo, "sha", sha, "contents", path) + if err != nil { + return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + } + case ref != "": + resourceURI, err = url.JoinPath("repo://", owner, repo, ref, "contents", path) + if err != nil { + return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + } + default: + resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path) + if err != nil { + return nil, nil, fmt.Errorf("failed to create resource URI: %w", err) + } + } + + // Determine if content is text or binary + isTextContent := strings.HasPrefix(contentType, "text/") || + contentType == "application/json" || + contentType == "application/xml" || + strings.HasSuffix(contentType, "+json") || + strings.HasSuffix(contentType, "+xml") + + if isTextContent { + result := &mcp.ResourceContents{ + URI: resourceURI, + Text: string(body), + MIMEType: contentType, + } + // Include SHA in the result metadata + if fileSHA != "" { + return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded text file (SHA: %s)", fileSHA), result), nil, nil + } + return utils.NewToolResultResource("successfully downloaded text file", result), nil, nil + } + + result := &mcp.ResourceContents{ + URI: resourceURI, + Blob: body, + MIMEType: contentType, + } + // Include SHA in the result metadata + if fileSHA != "" { + return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA), result), nil, nil + } + return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil } - return utils.NewToolResultResource("successfully downloaded text file", result), nil, nil + rawAPIResponseCode = resp.StatusCode } - result := &mcp.ResourceContents{ - URI: resourceURI, - Blob: body, - MIMEType: contentType, + if rawOpts.SHA != "" { + ref = rawOpts.SHA } - // Include SHA in the result metadata - if fileSHA != "" { - return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA), result), nil, nil + if strings.HasSuffix(path, "/") { + opts := &github.RepositoryContentGetOptions{Ref: ref} + _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) + if err == nil && resp.StatusCode == http.StatusOK { + defer func() { _ = resp.Body.Close() }() + r, err := json.Marshal(dirContent) + if err != nil { + return utils.NewToolResultError("failed to marshal response"), nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil + } } - return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil - } - rawAPIResponseCode = resp.StatusCode - } - if rawOpts.SHA != "" { - ref = rawOpts.SHA - } - if strings.HasSuffix(path, "/") { - opts := &github.RepositoryContentGetOptions{Ref: ref} - _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) - if err == nil && resp.StatusCode == http.StatusOK { - defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(dirContent) + // The path does not point to a file or directory. + // Instead let's try to find it in the Git Tree by matching the end of the path. + + // Step 1: Get Git Tree recursively + tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true) if err != nil { - return utils.NewToolResultError("failed to marshal response"), nil, nil + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get git tree", + resp, + err, + ), nil, nil } - return utils.NewToolResultText(string(r)), nil, nil - } - } + defer func() { _ = resp.Body.Close() }() - // The path does not point to a file or directory. - // Instead let's try to find it in the Git Tree by matching the end of the path. + // Step 2: Filter tree for matching paths + const maxMatchingFiles = 3 + matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles) + if len(matchingFiles) > 0 { + matchingFilesJSON, err := json.Marshal(matchingFiles) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil + } + resolvedRefs, err := json.Marshal(rawOpts) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil + } + return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil + } - // Step 1: Get Git Tree recursively - tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get git tree", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - // Step 2: Filter tree for matching paths - const maxMatchingFiles = 3 - matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles) - if len(matchingFiles) > 0 { - matchingFilesJSON, err := json.Marshal(matchingFiles) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil - } - resolvedRefs, err := json.Marshal(rawOpts) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil + return utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil - } - - return utils.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil, nil - }) - - return tool, handler + }, + ) } // ForkRepository creates a tool to fork a repository. -func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "fork_repository", - Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "organization": { - Type: "string", - Description: "Organization to fork to", +func ForkRepository(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "fork_repository", + Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "organization": { + Type: "string", + Description: "Organization to fork to", + }, }, + Required: []string{"owner", "repo"}, }, - Required: []string{"owner", "repo"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - org, err := OptionalParam[string](args, "organization") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.RepositoryCreateForkOptions{} - if org != "" { - opts.Organization = org - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + org, err := OptionalParam[string](args, "organization") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) - if err != nil { - // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, - // and it's not a real error. - if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { - return utils.NewToolResultText("Fork is in progress"), nil, nil - } - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to fork repository", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + opts := &github.RepositoryCreateForkOptions{} + if org != "" { + opts.Organization = org + } - if resp.StatusCode != http.StatusAccepted { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) + if err != nil { + // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, + // and it's not a real error. + if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { + return utils.NewToolResultText("Fork is in progress"), nil, nil + } + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to fork repository", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Return minimal response with just essential information - minimalResponse := MinimalResponse{ - ID: fmt.Sprintf("%d", forkedRepo.GetID()), - URL: forkedRepo.GetHTMLURL(), - } + if resp.StatusCode != http.StatusAccepted { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil, nil + } - r, err := json.Marshal(minimalResponse) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Return minimal response with just essential information + minimalResponse := MinimalResponse{ + ID: fmt.Sprintf("%d", forkedRepo.GetID()), + URL: forkedRepo.GetHTMLURL(), + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalResponse) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // DeleteFile creates a tool to delete a file in a GitHub repository. @@ -854,872 +862,880 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) // unlike how the endpoint backing the create_or_update_files tool does. This appears to be a quirk of the API. // The approach implemented here gets automatic commit signing when used with either the github-actions user or as an app, // both of which suit an LLM well. -func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "delete_file", - Description: t("TOOL_DELETE_FILE_DESCRIPTION", "Delete a file from a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_DELETE_FILE_USER_TITLE", "Delete file"), - ReadOnlyHint: false, - DestructiveHint: github.Ptr(true), - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner (username or organization)", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "path": { - Type: "string", - Description: "Path to the file to delete", - }, - "message": { - Type: "string", - Description: "Commit message", - }, - "branch": { - Type: "string", - Description: "Branch to delete the file from", +func DeleteFile(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "delete_file", + Description: t("TOOL_DELETE_FILE_DESCRIPTION", "Delete a file from a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_DELETE_FILE_USER_TITLE", "Delete file"), + ReadOnlyHint: false, + DestructiveHint: github.Ptr(true), + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "path": { + Type: "string", + Description: "Path to the file to delete", + }, + "message": { + Type: "string", + Description: "Commit message", + }, + "branch": { + Type: "string", + Description: "Branch to delete the file from", + }, }, + Required: []string{"owner", "repo", "path", "message", "branch"}, }, - Required: []string{"owner", "repo", "path", "message", "branch"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - path, err := RequiredParam[string](args, "path") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - message, err := RequiredParam[string](args, "message") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - branch, err := RequiredParam[string](args, "branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + path, err := RequiredParam[string](args, "path") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + message, err := RequiredParam[string](args, "message") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + branch, err := RequiredParam[string](args, "branch") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - // Get the reference for the branch - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) - if err != nil { - return nil, nil, fmt.Errorf("failed to get branch reference: %w", err) - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - // Get the commit object that the branch points to - baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get base commit", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Get the reference for the branch + ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) + if err != nil { + return nil, nil, fmt.Errorf("failed to get branch reference: %w", err) + } + defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil - } + // Get the commit object that the branch points to + baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get base commit", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Create a tree entry for the file deletion by setting SHA to nil - treeEntries := []*github.TreeEntry{ - { - Path: github.Ptr(path), - Mode: github.Ptr("100644"), // Regular file mode - Type: github.Ptr("blob"), - SHA: nil, // Setting SHA to nil deletes the file - }, - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil, nil + } - // Create a new tree with the deletion - newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, treeEntries) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create tree", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Create a tree entry for the file deletion by setting SHA to nil + treeEntries := []*github.TreeEntry{ + { + Path: github.Ptr(path), + Mode: github.Ptr("100644"), // Regular file mode + Type: github.Ptr("blob"), + SHA: nil, // Setting SHA to nil deletes the file + }, + } - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to create tree: %s", string(body))), nil, nil - } + // Create a new tree with the deletion + newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, treeEntries) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create tree", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Create a new commit with the new tree - commit := github.Commit{ - Message: github.Ptr(message), - Tree: newTree, - Parents: []*github.Commit{{SHA: baseCommit.SHA}}, - } - newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create commit", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to create tree: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to create commit: %s", string(body))), nil, nil - } + // Create a new commit with the new tree + commit := github.Commit{ + Message: github.Ptr(message), + Tree: newTree, + Parents: []*github.Commit{{SHA: baseCommit.SHA}}, + } + newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create commit", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Update the branch reference to point to the new commit - ref.Object.SHA = newCommit.SHA - _, resp, err = client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{ - SHA: *newCommit.SHA, - Force: github.Ptr(false), - }) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to update reference", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to create commit: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to update reference: %s", string(body))), nil, nil - } + // Update the branch reference to point to the new commit + ref.Object.SHA = newCommit.SHA + _, resp, err = client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{ + SHA: *newCommit.SHA, + Force: github.Ptr(false), + }) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to update reference", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Create a response similar to what the DeleteFile API would return - response := map[string]interface{}{ - "commit": newCommit, - "content": nil, - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to update reference: %s", string(body))), nil, nil + } - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Create a response similar to what the DeleteFile API would return + response := map[string]interface{}{ + "commit": newCommit, + "content": nil, + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // CreateBranch creates a tool to create a new branch. -func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "create_branch", - Description: t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "branch": { - Type: "string", - Description: "Name for new branch", - }, - "from_branch": { - Type: "string", - Description: "Source branch (defaults to repo default)", +func CreateBranch(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "create_branch", + Description: t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "branch": { + Type: "string", + Description: "Name for new branch", + }, + "from_branch": { + Type: "string", + Description: "Source branch (defaults to repo default)", + }, }, + Required: []string{"owner", "repo", "branch"}, }, - Required: []string{"owner", "repo", "branch"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - branch, err := RequiredParam[string](args, "branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - fromBranch, err := OptionalParam[string](args, "from_branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + branch, err := RequiredParam[string](args, "branch") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + fromBranch, err := OptionalParam[string](args, "from_branch") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - // Get the source branch SHA - var ref *github.Reference - - if fromBranch == "" { - // Get default branch if from_branch not specified - repository, resp, err := client.Repositories.Get(ctx, owner, repo) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get repository", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Get the source branch SHA + var ref *github.Reference - fromBranch = *repository.DefaultBranch - } + if fromBranch == "" { + // Get default branch if from_branch not specified + repository, resp, err := client.Repositories.Get(ctx, owner, repo) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get repository", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Get SHA of source branch - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+fromBranch) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get reference", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + fromBranch = *repository.DefaultBranch + } - // Create new branch - newRef := github.CreateRef{ - Ref: "refs/heads/" + branch, - SHA: *ref.Object.SHA, - } + // Get SHA of source branch + ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+fromBranch) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get reference", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - createdRef, resp, err := client.Git.CreateRef(ctx, owner, repo, newRef) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create branch", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Create new branch + newRef := github.CreateRef{ + Ref: "refs/heads/" + branch, + SHA: *ref.Object.SHA, + } - r, err := json.Marshal(createdRef) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + createdRef, resp, err := client.Git.CreateRef(ctx, owner, repo, newRef) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create branch", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(createdRef) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // PushFiles creates a tool to push multiple files in a single commit to a GitHub repository. -func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "push_files", - Description: t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "branch": { - Type: "string", - Description: "Branch to push to", - }, - "files": { - Type: "array", - Description: "Array of file objects to push, each object with path (string) and content (string)", - Items: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "path": { - Type: "string", - Description: "path to the file", - }, - "content": { - Type: "string", - Description: "file content", +func PushFiles(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "push_files", + Description: t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "branch": { + Type: "string", + Description: "Branch to push to", + }, + "files": { + Type: "array", + Description: "Array of file objects to push, each object with path (string) and content (string)", + Items: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "path": { + Type: "string", + Description: "path to the file", + }, + "content": { + Type: "string", + Description: "file content", + }, }, + Required: []string{"path", "content"}, }, - Required: []string{"path", "content"}, + }, + "message": { + Type: "string", + Description: "Commit message", }, }, - "message": { - Type: "string", - Description: "Commit message", - }, + Required: []string{"owner", "repo", "branch", "files", "message"}, }, - Required: []string{"owner", "repo", "branch", "files", "message"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - branch, err := RequiredParam[string](args, "branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - message, err := RequiredParam[string](args, "message") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + branch, err := RequiredParam[string](args, "branch") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + message, err := RequiredParam[string](args, "message") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - // Parse files parameter - this should be an array of objects with path and content - filesObj, ok := args["files"].([]interface{}) - if !ok { - return utils.NewToolResultError("files parameter must be an array of objects with path and content"), nil, nil - } + // Parse files parameter - this should be an array of objects with path and content + filesObj, ok := args["files"].([]interface{}) + if !ok { + return utils.NewToolResultError("files parameter must be an array of objects with path and content"), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - // Get the reference for the branch - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get branch reference", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Get the reference for the branch + ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get branch reference", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Get the commit object that the branch points to - baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get base commit", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Get the commit object that the branch points to + baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get base commit", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Create tree entries for all files - var entries []*github.TreeEntry + // Create tree entries for all files + var entries []*github.TreeEntry - for _, file := range filesObj { - fileMap, ok := file.(map[string]interface{}) - if !ok { - return utils.NewToolResultError("each file must be an object with path and content"), nil, nil - } + for _, file := range filesObj { + fileMap, ok := file.(map[string]interface{}) + if !ok { + return utils.NewToolResultError("each file must be an object with path and content"), nil, nil + } - path, ok := fileMap["path"].(string) - if !ok || path == "" { - return utils.NewToolResultError("each file must have a path"), nil, nil - } + path, ok := fileMap["path"].(string) + if !ok || path == "" { + return utils.NewToolResultError("each file must have a path"), nil, nil + } - content, ok := fileMap["content"].(string) - if !ok { - return utils.NewToolResultError("each file must have content"), nil, nil - } + content, ok := fileMap["content"].(string) + if !ok { + return utils.NewToolResultError("each file must have content"), nil, nil + } - // Create a tree entry for the file - entries = append(entries, &github.TreeEntry{ - Path: github.Ptr(path), - Mode: github.Ptr("100644"), // Regular file mode - Type: github.Ptr("blob"), - Content: github.Ptr(content), - }) - } + // Create a tree entry for the file + entries = append(entries, &github.TreeEntry{ + Path: github.Ptr(path), + Mode: github.Ptr("100644"), // Regular file mode + Type: github.Ptr("blob"), + Content: github.Ptr(content), + }) + } - // Create a new tree with the file entries - newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create tree", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Create a new tree with the file entries + newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create tree", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Create a new commit - commit := github.Commit{ - Message: github.Ptr(message), - Tree: newTree, - Parents: []*github.Commit{{SHA: baseCommit.SHA}}, - } - newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to create commit", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - // Update the reference to point to the new commit - ref.Object.SHA = newCommit.SHA - updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{ - SHA: *newCommit.SHA, - Force: github.Ptr(false), - }) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to update reference", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + // Create a new commit + commit := github.Commit{ + Message: github.Ptr(message), + Tree: newTree, + Parents: []*github.Commit{{SHA: baseCommit.SHA}}, + } + newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to create commit", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(updatedRef) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + // Update the reference to point to the new commit + ref.Object.SHA = newCommit.SHA + updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{ + SHA: *newCommit.SHA, + Force: github.Ptr(false), + }) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to update reference", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(updatedRef) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // ListTags creates a tool to list tags in a GitHub repository. -func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "list_tags", - Description: t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, +func ListTags(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "list_tags", + Description: t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"), + ReadOnlyHint: true, }, - Required: []string{"owner", "repo"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, - } + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + }, + Required: []string{"owner", "repo"}, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + opts := &github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + } - tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list tags", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil, nil - } + tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list tags", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(tags) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(tags) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // GetTag creates a tool to get details about a specific tag in a GitHub repository. -func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_tag", - Description: t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "tag": { - Type: "string", - Description: "Tag name", +func GetTag(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "get_tag", + Description: t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "tag": { + Type: "string", + Description: "Tag name", + }, }, + Required: []string{"owner", "repo", "tag"}, }, - Required: []string{"owner", "repo", "tag"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - tag, err := RequiredParam[string](args, "tag") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + tag, err := RequiredParam[string](args, "tag") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - // First get the tag reference - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get tag reference", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get tag reference: %s", string(body))), nil, nil - } + // First get the tag reference + ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/tags/"+tag) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get tag reference", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Then get the tag object - tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get tag object", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get tag reference: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get tag object: %s", string(body))), nil, nil - } + // Then get the tag object + tagObj, resp, err := client.Git.GetTag(ctx, owner, repo, *ref.Object.SHA) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get tag object", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(tagObj) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get tag object: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(tagObj) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // ListReleases creates a tool to list releases in a GitHub repository. -func ListReleases(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "list_releases", - Description: t("TOOL_LIST_RELEASES_DESCRIPTION", "List releases in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_RELEASES_USER_TITLE", "List releases"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, +func ListReleases(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "list_releases", + Description: t("TOOL_LIST_RELEASES_DESCRIPTION", "List releases in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_RELEASES_USER_TITLE", "List releases"), + ReadOnlyHint: true, }, - Required: []string{"owner", "repo"}, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, - } + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + }, + Required: []string{"owner", "repo"}, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + opts := &github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + } - releases, resp, err := client.Repositories.ListReleases(ctx, owner, repo, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list releases: %w", err) - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list releases: %s", string(body))), nil, nil - } + releases, resp, err := client.Repositories.ListReleases(ctx, owner, repo, opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to list releases: %w", err) + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(releases) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list releases: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(releases) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // GetLatestRelease creates a tool to get the latest release in a GitHub repository. -func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_latest_release", - Description: t("TOOL_GET_LATEST_RELEASE_DESCRIPTION", "Get the latest release in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_LATEST_RELEASE_USER_TITLE", "Get latest release"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", +func GetLatestRelease(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "get_latest_release", + Description: t("TOOL_GET_LATEST_RELEASE_DESCRIPTION", "Get the latest release in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_LATEST_RELEASE_USER_TITLE", "Get latest release"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, }, + Required: []string{"owner", "repo"}, }, - Required: []string{"owner", "repo"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - release, resp, err := client.Repositories.GetLatestRelease(ctx, owner, repo) - if err != nil { - return nil, nil, fmt.Errorf("failed to get latest release: %w", err) - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get latest release: %s", string(body))), nil, nil - } + release, resp, err := client.Repositories.GetLatestRelease(ctx, owner, repo) + if err != nil { + return nil, nil, fmt.Errorf("failed to get latest release: %w", err) + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(release) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get latest release: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(release) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func GetReleaseByTag(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "get_release_by_tag", - Description: t("TOOL_GET_RELEASE_BY_TAG_DESCRIPTION", "Get a specific release by its tag name in a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_RELEASE_BY_TAG_USER_TITLE", "Get a release by tag name"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", - }, - "tag": { - Type: "string", - Description: "Tag name (e.g., 'v1.0.0')", +func GetReleaseByTag(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "get_release_by_tag", + Description: t("TOOL_GET_RELEASE_BY_TAG_DESCRIPTION", "Get a specific release by its tag name in a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_RELEASE_BY_TAG_USER_TITLE", "Get a release by tag name"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "tag": { + Type: "string", + Description: "Tag name (e.g., 'v1.0.0')", + }, }, + Required: []string{"owner", "repo", "tag"}, }, - Required: []string{"owner", "repo", "tag"}, }, - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - tag, err := RequiredParam[string](args, "tag") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + tag, err := RequiredParam[string](args, "tag") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - release, resp, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to get release by tag: %s", tag), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to get release by tag: %s", string(body))), nil, nil - } + release, resp, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get release by tag: %s", tag), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(release) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get release by tag: %s", string(body))), nil, nil + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(release) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // filterPaths filters the entries in a GitHub tree to find paths that @@ -1858,260 +1874,263 @@ func resolveGitReference(ctx context.Context, githubClient *github.Client, owner } // ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user. -func ListStarredRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "list_starred_repositories", - Description: t("TOOL_LIST_STARRED_REPOSITORIES_DESCRIPTION", "List starred repositories"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_STARRED_REPOSITORIES_USER_TITLE", "List starred repositories"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "username": { - Type: "string", - Description: "Username to list starred repositories for. Defaults to the authenticated user.", - }, - "sort": { - Type: "string", - Description: "How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to).", - Enum: []any{"created", "updated"}, - }, - "direction": { - Type: "string", - Description: "The direction to sort the results by.", - Enum: []any{"asc", "desc"}, - }, - }, - }), - } - - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - username, err := OptionalParam[string](args, "username") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - sort, err := OptionalParam[string](args, "sort") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - direction, err := OptionalParam[string](args, "direction") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - opts := &github.ActivityListStarredOptions{ - ListOptions: github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, +func ListStarredRepositories(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "list_starred_repositories", + Description: t("TOOL_LIST_STARRED_REPOSITORIES_DESCRIPTION", "List starred repositories"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_STARRED_REPOSITORIES_USER_TITLE", "List starred repositories"), + ReadOnlyHint: true, }, - } - if sort != "" { - opts.Sort = sort - } - if direction != "" { - opts.Direction = direction - } + InputSchema: WithPagination(&jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "username": { + Type: "string", + Description: "Username to list starred repositories for. Defaults to the authenticated user.", + }, + "sort": { + Type: "string", + Description: "How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to).", + Enum: []any{"created", "updated"}, + }, + "direction": { + Type: "string", + Description: "The direction to sort the results by.", + Enum: []any{"asc", "desc"}, + }, + }, + }), + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + username, err := OptionalParam[string](args, "username") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + sort, err := OptionalParam[string](args, "sort") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + direction, err := OptionalParam[string](args, "direction") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + opts := &github.ActivityListStarredOptions{ + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + }, + } + if sort != "" { + opts.Sort = sort + } + if direction != "" { + opts.Direction = direction + } - var repos []*github.StarredRepository - var resp *github.Response - if username == "" { - // List starred repositories for the authenticated user - repos, resp, err = client.Activity.ListStarred(ctx, "", opts) - } else { - // List starred repositories for a specific user - repos, resp, err = client.Activity.ListStarred(ctx, username, opts) - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to list starred repositories for user '%s'", username), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + var repos []*github.StarredRepository + var resp *github.Response + if username == "" { + // List starred repositories for the authenticated user + repos, resp, err = client.Activity.ListStarred(ctx, "", opts) + } else { + // List starred repositories for a specific user + repos, resp, err = client.Activity.ListStarred(ctx, username, opts) + } - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return utils.NewToolResultError(fmt.Sprintf("failed to list starred repositories: %s", string(body))), nil, nil - } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list starred repositories for user '%s'", username), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - // Convert to minimal format - minimalRepos := make([]MinimalRepository, 0, len(repos)) - for _, starredRepo := range repos { - repo := starredRepo.Repository - minimalRepo := MinimalRepository{ - ID: repo.GetID(), - Name: repo.GetName(), - FullName: repo.GetFullName(), - Description: repo.GetDescription(), - HTMLURL: repo.GetHTMLURL(), - Language: repo.GetLanguage(), - Stars: repo.GetStargazersCount(), - Forks: repo.GetForksCount(), - OpenIssues: repo.GetOpenIssuesCount(), - Private: repo.GetPrivate(), - Fork: repo.GetFork(), - Archived: repo.GetArchived(), - DefaultBranch: repo.GetDefaultBranch(), - } + if resp.StatusCode != 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to list starred repositories: %s", string(body))), nil, nil + } - if repo.UpdatedAt != nil { - minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z") - } + // Convert to minimal format + minimalRepos := make([]MinimalRepository, 0, len(repos)) + for _, starredRepo := range repos { + repo := starredRepo.Repository + minimalRepo := MinimalRepository{ + ID: repo.GetID(), + Name: repo.GetName(), + FullName: repo.GetFullName(), + Description: repo.GetDescription(), + HTMLURL: repo.GetHTMLURL(), + Language: repo.GetLanguage(), + Stars: repo.GetStargazersCount(), + Forks: repo.GetForksCount(), + OpenIssues: repo.GetOpenIssuesCount(), + Private: repo.GetPrivate(), + Fork: repo.GetFork(), + Archived: repo.GetArchived(), + DefaultBranch: repo.GetDefaultBranch(), + } - minimalRepos = append(minimalRepos, minimalRepo) - } + if repo.UpdatedAt != nil { + minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z") + } - r, err := json.Marshal(minimalRepos) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal starred repositories: %w", err) - } + minimalRepos = append(minimalRepos, minimalRepo) + } - return utils.NewToolResultText(string(r)), nil, nil - }) + r, err := json.Marshal(minimalRepos) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal starred repositories: %w", err) + } - return tool, handler + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } // StarRepository creates a tool to star a repository. -func StarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "star_repository", - Description: t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", +func StarRepository(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "star_repository", + Description: t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, }, + Required: []string{"owner", "repo"}, }, - Required: []string{"owner", "repo"}, }, - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + resp, err := client.Activity.Star(ctx, owner, repo) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to star repository %s/%s", owner, repo), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - resp, err := client.Activity.Star(ctx, owner, repo) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to star repository %s/%s", owner, repo), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != 204 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to star repository: %s", string(body))), nil, nil + } - if resp.StatusCode != 204 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultText(fmt.Sprintf("Successfully starred repository %s/%s", owner, repo)), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to star repository: %s", string(body))), nil, nil - } - - return utils.NewToolResultText(fmt.Sprintf("Successfully starred repository %s/%s", owner, repo)), nil, nil - }) - - return tool, handler + }, + ) } // UnstarRepository creates a tool to unstar a repository. -func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - tool := mcp.Tool{ - Name: "unstar_repository", - Description: t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: "Repository owner", - }, - "repo": { - Type: "string", - Description: "Repository name", +func UnstarRepository(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ + Name: "unstar_repository", + Description: t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, }, + Required: []string{"owner", "repo"}, }, - Required: []string{"owner", "repo"}, }, - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } - client, err := getClient(ctx) - if err != nil { - return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + resp, err := client.Activity.Unstar(ctx, owner, repo) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to unstar repository %s/%s", owner, repo), + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - resp, err := client.Activity.Unstar(ctx, owner, repo) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to unstar repository %s/%s", owner, repo), - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != 204 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to unstar repository: %s", string(body))), nil, nil + } - if resp.StatusCode != 204 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("failed to unstar repository: %s", string(body))), nil, nil - } - - return utils.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil, nil - }) - - return tool, handler + }, + ) } diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 7e76d4230..b4ccd3603 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -23,9 +23,8 @@ import ( func Test_GetFileContents(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - mockRawClient := raw.NewClient(mockClient, &url.URL{Scheme: "https", Host: "raw.githubusercontent.com", Path: "/"}) - tool, _ := GetFileContents(stubGetClientFn(mockClient), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + serverTool := GetFileContents(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -287,18 +286,23 @@ func Test_GetFileContents(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) mockRawClient := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"}) - _, handler := GetFileContents(stubGetClientFn(client), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + GetRawClient: stubGetRawClientFn(mockRawClient), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrMsg) + require.NoError(t, err) + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) return } @@ -331,8 +335,8 @@ func Test_GetFileContents(t *testing.T) { func Test_ForkRepository(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := ForkRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ForkRepository(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -406,13 +410,16 @@ func Test_ForkRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ForkRepository(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -436,8 +443,8 @@ func Test_ForkRepository(t *testing.T) { func Test_CreateBranch(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := CreateBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := CreateBranch(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -599,13 +606,16 @@ func Test_CreateBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateBranch(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -634,8 +644,8 @@ func Test_CreateBranch(t *testing.T) { func Test_GetCommit(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := GetCommit(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetCommit(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -728,13 +738,16 @@ func Test_GetCommit(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetCommit(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -766,8 +779,8 @@ func Test_GetCommit(t *testing.T) { func Test_ListCommits(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := ListCommits(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListCommits(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -951,13 +964,16 @@ func Test_ListCommits(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListCommits(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -999,8 +1015,8 @@ func Test_ListCommits(t *testing.T) { func Test_CreateOrUpdateFile(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := CreateOrUpdateFile(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := CreateOrUpdateFile(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -1127,13 +1143,16 @@ func Test_CreateOrUpdateFile(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateOrUpdateFile(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -1169,8 +1188,8 @@ func Test_CreateOrUpdateFile(t *testing.T) { func Test_CreateRepository(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := CreateRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := CreateRepository(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -1310,13 +1329,16 @@ func Test_CreateRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateRepository(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -1346,8 +1368,8 @@ func Test_CreateRepository(t *testing.T) { func Test_PushFiles(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := PushFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := PushFiles(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -1646,13 +1668,16 @@ func Test_PushFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := PushFiles(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -1690,8 +1715,8 @@ func Test_PushFiles(t *testing.T) { func Test_ListBranches(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListBranches(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -1764,17 +1789,21 @@ func Test_ListBranches(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create mock client mockClient := github.NewClient(mock.NewMockedHTTPClient(tt.mockResponses...)) - _, handler := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(mockClient), + } + handler := serverTool.Handler(deps) // Create request request := createMCPRequest(tt.args) // Call handler - result, _, err := handler(context.Background(), &request, tt.args) + result, err := handler(context.Background(), &request) if tt.wantErr { - require.Error(t, err) + require.NoError(t, err) if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tt.errContains) } return } @@ -1804,8 +1833,8 @@ func Test_ListBranches(t *testing.T) { func Test_DeleteFile(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := DeleteFile(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := DeleteFile(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -1948,13 +1977,16 @@ func Test_DeleteFile(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := DeleteFile(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -1985,8 +2017,8 @@ func Test_DeleteFile(t *testing.T) { func Test_ListTags(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := ListTags(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListTags(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -2072,13 +2104,16 @@ func Test_ListTags(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListTags(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -2112,8 +2147,8 @@ func Test_ListTags(t *testing.T) { func Test_GetTag(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := GetTag(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetTag(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -2229,13 +2264,16 @@ func Test_GetTag(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetTag(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -2267,8 +2305,8 @@ func Test_GetTag(t *testing.T) { } func Test_ListReleases(t *testing.T) { - mockClient := github.NewClient(nil) - tool, _ := ListReleases(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListReleases(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -2339,9 +2377,12 @@ func Test_ListReleases(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := ListReleases(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) if tc.expectError { require.Error(t, err) @@ -2362,8 +2403,8 @@ func Test_ListReleases(t *testing.T) { } } func Test_GetLatestRelease(t *testing.T) { - mockClient := github.NewClient(nil) - tool, _ := GetLatestRelease(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetLatestRelease(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -2427,9 +2468,12 @@ func Test_GetLatestRelease(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := GetLatestRelease(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) if tc.expectError { require.Error(t, err) @@ -2448,8 +2492,8 @@ func Test_GetLatestRelease(t *testing.T) { } func Test_GetReleaseByTag(t *testing.T) { - mockClient := github.NewClient(nil) - tool, _ := GetReleaseByTag(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetReleaseByTag(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -2572,11 +2616,14 @@ func Test_GetReleaseByTag(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - _, handler := GetReleaseByTag(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) if tc.expectError { require.Error(t, err) @@ -2960,8 +3007,8 @@ func Test_resolveGitReference(t *testing.T) { func Test_ListStarredRepositories(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := ListStarredRepositories(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListStarredRepositories(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -3081,13 +3128,16 @@ func Test_ListStarredRepositories(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListStarredRepositories(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -3119,8 +3169,8 @@ func Test_ListStarredRepositories(t *testing.T) { func Test_StarRepository(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := StarRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := StarRepository(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -3179,13 +3229,16 @@ func Test_StarRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := StarRepository(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { @@ -3207,8 +3260,8 @@ func Test_StarRepository(t *testing.T) { func Test_UnstarRepository(t *testing.T) { // Verify tool definition once - mockClient := github.NewClient(nil) - tool, _ := UnstarRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := UnstarRepository(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) schema, ok := tool.InputSchema.(*jsonschema.Schema) @@ -3267,13 +3320,16 @@ func Test_UnstarRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := UnstarRepository(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) // Create call request request := createMCPRequest(tc.requestArgs) // Call handler - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) // Verify results if tc.expectError { diff --git a/pkg/github/tools.go b/pkg/github/tools.go index d9008a912..3bd210495 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -180,24 +180,24 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG SetDependencies(deps). AddReadTools( SearchRepositories(t), - toolsets.NewServerToolLegacy(GetFileContents(getClient, getRawClient, t)), - toolsets.NewServerToolLegacy(ListCommits(getClient, t)), + GetFileContents(t), + ListCommits(t), SearchCode(t), - toolsets.NewServerToolLegacy(GetCommit(getClient, t)), - toolsets.NewServerToolLegacy(ListBranches(getClient, t)), - toolsets.NewServerToolLegacy(ListTags(getClient, t)), - toolsets.NewServerToolLegacy(GetTag(getClient, t)), - toolsets.NewServerToolLegacy(ListReleases(getClient, t)), - toolsets.NewServerToolLegacy(GetLatestRelease(getClient, t)), - toolsets.NewServerToolLegacy(GetReleaseByTag(getClient, t)), + GetCommit(t), + ListBranches(t), + ListTags(t), + GetTag(t), + ListReleases(t), + GetLatestRelease(t), + GetReleaseByTag(t), ). AddWriteTools( - toolsets.NewServerToolLegacy(CreateOrUpdateFile(getClient, t)), - toolsets.NewServerToolLegacy(CreateRepository(getClient, t)), - toolsets.NewServerToolLegacy(ForkRepository(getClient, t)), - toolsets.NewServerToolLegacy(CreateBranch(getClient, t)), - toolsets.NewServerToolLegacy(PushFiles(getClient, t)), - toolsets.NewServerToolLegacy(DeleteFile(getClient, t)), + CreateOrUpdateFile(t), + CreateRepository(t), + ForkRepository(t), + CreateBranch(t), + PushFiles(t), + DeleteFile(t), ). AddResourceTemplates( toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), @@ -278,14 +278,14 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description). SetDependencies(deps). AddReadTools( - toolsets.NewServerToolLegacy(ListNotifications(getClient, t)), - toolsets.NewServerToolLegacy(GetNotificationDetails(getClient, t)), + ListNotifications(t), + GetNotificationDetails(t), ). AddWriteTools( - toolsets.NewServerToolLegacy(DismissNotification(getClient, t)), - toolsets.NewServerToolLegacy(MarkAllNotificationsRead(getClient, t)), - toolsets.NewServerToolLegacy(ManageNotificationSubscription(getClient, t)), - toolsets.NewServerToolLegacy(ManageRepositoryNotificationSubscription(getClient, t)), + DismissNotification(t), + MarkAllNotificationsRead(t), + ManageNotificationSubscription(t), + ManageRepositoryNotificationSubscription(t), ) discussions := toolsets.NewToolset(ToolsetMetadataDiscussions.ID, ToolsetMetadataDiscussions.Description). @@ -368,11 +368,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). SetDependencies(deps). AddReadTools( - toolsets.NewServerToolLegacy(ListStarredRepositories(getClient, t)), + ListStarredRepositories(t), ). AddWriteTools( - toolsets.NewServerToolLegacy(StarRepository(getClient, t)), - toolsets.NewServerToolLegacy(UnstarRepository(getClient, t)), + StarRepository(t), + UnstarRepository(t), ) labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). SetDependencies(deps).