8cd03c0185
Docker / build (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
MCP Server Diff / mcp-diff (push) Has been cancelled
MCP Server Diff / mcp-diff-http (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
* Add reaction tools for issues and pull requests Implement three granular-only tools for adding emoji reactions: - add_issue_reaction: Add reaction to an issue - add_issue_comment_reaction: Add reaction to an issue comment - add_pull_request_review_comment_reaction: Add reaction to a PR review comment All tools are feature-flagged with FeatureFlagEnable set to enable them only when clients request granular toolsets. Tools use go-github's Reactions service and return minimal ID response on success (HTTP 201). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make reaction tools available in both granular and non-granular modes Remove feature flag gates from reaction tools so they're available to all clients regardless of granular toolset preference. Reaction tools are naturally atomic operations and work equally well in both modes. Updates test expectations to exclude reaction tools from granular-only test assertions, since they're now always available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update toolsnaps with aligned reaction tool descriptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align reaction tool descriptions with codebase style Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve reaction tool responses and wording Return reaction URLs in minimal responses and clarify issue tools apply to pull requests where applicable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Expose reactions through default comment tools Keep the standalone reaction tools behind granular feature flags to avoid expanding the default tool count. Add optional reaction support to the existing issue comment and pull request comment reply tools, requiring at least one of body or reaction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify PR review comment IDs for reactions Document that PR review comment reaction inputs require the numeric review comment ID, not the GraphQL review thread node ID returned by review thread APIs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Support issue comment reactions in add_issue_comment Add optional comment_id support so the default add_issue_comment tool can react to a specific issue or pull request comment without exposing a separate default reaction tool. Keep body creation tied to issue_number and require reaction targets to provide either issue_number or comment_id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden combined comment reaction tools Apply reactions before creating comments or replies so retrying a failed combined call cannot duplicate the non-idempotent comment operation. Also reject issue comment IDs without a reaction target to avoid silently ignoring the field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Group reaction tools with granular registrations Keep standalone reaction tool registrations next to the granular issue and pull request tools they belong with, so the default compound tool sections stay focused. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align granular reaction tool constructors Rename the standalone reaction tool constructors to match the granular tool naming convention and clarify issue comment reaction IDs for pull request comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Require issue number for comment reactions Make issue_number required on the consolidated add_issue_comment tool even when reacting to a specific issue comment, keeping the default tool input shape explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address issue comment feedback Return structured GitHub API errors for issue comment creation failures and assert MCP error results in tests. Also reject comment_id with body on the consolidated comment tool to avoid ambiguous comment-plus-comment-reaction requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate issue comment reaction target Use the required issue_number to verify issue comment reaction targets before creating the reaction, and remove overlapping add_issue_comment test coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate positive comment IDs Reject non-positive issue and pull request comment IDs in handlers and add the missing schema minimum for pull request review comment IDs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Sam Morrow <sammorrowdrums@github.com> Co-authored-by: Sam Morrow <info@sam-morrow.com>
505 lines
15 KiB
Go
505 lines
15 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/google/go-github/v87/github"
|
|
"github.com/shurcooL/githubv4"
|
|
|
|
"github.com/github/github-mcp-server/pkg/inventory"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
)
|
|
|
|
type GetClientFn func(context.Context) (*github.Client, error)
|
|
type GetGQLClientFn func(context.Context) (*githubv4.Client, error)
|
|
|
|
// Toolset metadata constants - these define all available toolsets and their descriptions.
|
|
// Tools use these constants to declare which toolset they belong to.
|
|
// Icons are Octicon names from https://primer.style/foundations/icons
|
|
var (
|
|
ToolsetMetadataAll = inventory.ToolsetMetadata{
|
|
ID: "all",
|
|
Description: "Special toolset that enables all available toolsets",
|
|
Icon: "apps",
|
|
}
|
|
ToolsetMetadataDefault = inventory.ToolsetMetadata{
|
|
ID: "default",
|
|
Description: "Special toolset that enables the default toolset configuration. When no toolsets are specified, this is the set that is enabled",
|
|
Icon: "check-circle",
|
|
}
|
|
ToolsetMetadataContext = inventory.ToolsetMetadata{
|
|
ID: "context",
|
|
Description: "Tools that provide context about the current user and GitHub context you are operating in",
|
|
Default: true,
|
|
Icon: "person",
|
|
InstructionsFunc: generateContextToolsetInstructions,
|
|
}
|
|
ToolsetMetadataRepos = inventory.ToolsetMetadata{
|
|
ID: "repos",
|
|
Description: "GitHub Repository related tools",
|
|
Default: true,
|
|
Icon: "repo",
|
|
}
|
|
ToolsetMetadataGit = inventory.ToolsetMetadata{
|
|
ID: "git",
|
|
Description: "GitHub Git API related tools for low-level Git operations",
|
|
Icon: "git-branch",
|
|
}
|
|
ToolsetMetadataIssues = inventory.ToolsetMetadata{
|
|
ID: "issues",
|
|
Description: "GitHub Issues related tools",
|
|
Default: true,
|
|
Icon: "issue-opened",
|
|
InstructionsFunc: generateIssuesToolsetInstructions,
|
|
}
|
|
ToolsetMetadataPullRequests = inventory.ToolsetMetadata{
|
|
ID: "pull_requests",
|
|
Description: "GitHub Pull Request related tools",
|
|
Default: true,
|
|
Icon: "git-pull-request",
|
|
InstructionsFunc: generatePullRequestsToolsetInstructions,
|
|
}
|
|
ToolsetMetadataUsers = inventory.ToolsetMetadata{
|
|
ID: "users",
|
|
Description: "GitHub User related tools",
|
|
Default: true,
|
|
Icon: "people",
|
|
}
|
|
ToolsetMetadataOrgs = inventory.ToolsetMetadata{
|
|
ID: "orgs",
|
|
Description: "GitHub Organization related tools",
|
|
Icon: "organization",
|
|
}
|
|
ToolsetMetadataActions = inventory.ToolsetMetadata{
|
|
ID: "actions",
|
|
Description: "GitHub Actions workflows and CI/CD operations",
|
|
Icon: "workflow",
|
|
}
|
|
ToolsetMetadataCodeQuality = inventory.ToolsetMetadata{
|
|
ID: "code_quality",
|
|
Description: "GitHub Code Quality related tools",
|
|
Icon: "code-square",
|
|
}
|
|
ToolsetMetadataCodeSecurity = inventory.ToolsetMetadata{
|
|
ID: "code_security",
|
|
Description: "Code security related tools, such as GitHub Code Scanning",
|
|
Icon: "codescan",
|
|
}
|
|
ToolsetMetadataSecretProtection = inventory.ToolsetMetadata{
|
|
ID: "secret_protection",
|
|
Description: "Secret protection related tools, such as GitHub Secret Scanning",
|
|
Icon: "shield-lock",
|
|
}
|
|
ToolsetMetadataDependabot = inventory.ToolsetMetadata{
|
|
ID: "dependabot",
|
|
Description: "Dependabot tools",
|
|
Icon: "dependabot",
|
|
}
|
|
ToolsetMetadataNotifications = inventory.ToolsetMetadata{
|
|
ID: "notifications",
|
|
Description: "GitHub Notifications related tools",
|
|
Icon: "bell",
|
|
}
|
|
ToolsetMetadataDiscussions = inventory.ToolsetMetadata{
|
|
ID: "discussions",
|
|
Description: "GitHub Discussions related tools",
|
|
Icon: "comment-discussion",
|
|
InstructionsFunc: generateDiscussionsToolsetInstructions,
|
|
}
|
|
ToolsetMetadataGists = inventory.ToolsetMetadata{
|
|
ID: "gists",
|
|
Description: "GitHub Gist related tools",
|
|
Icon: "logo-gist",
|
|
}
|
|
ToolsetMetadataSecurityAdvisories = inventory.ToolsetMetadata{
|
|
ID: "security_advisories",
|
|
Description: "Security advisories related tools",
|
|
Icon: "shield",
|
|
}
|
|
ToolsetMetadataProjects = inventory.ToolsetMetadata{
|
|
ID: "projects",
|
|
Description: "GitHub Projects related tools",
|
|
Icon: "project",
|
|
InstructionsFunc: generateProjectsToolsetInstructions,
|
|
}
|
|
ToolsetMetadataStargazers = inventory.ToolsetMetadata{
|
|
ID: "stargazers",
|
|
Description: "GitHub Stargazers related tools",
|
|
Icon: "star",
|
|
}
|
|
ToolsetLabels = inventory.ToolsetMetadata{
|
|
ID: "labels",
|
|
Description: "GitHub Labels related tools",
|
|
Icon: "tag",
|
|
}
|
|
|
|
ToolsetMetadataCopilot = inventory.ToolsetMetadata{
|
|
ID: "copilot",
|
|
Description: "Copilot related tools",
|
|
Default: true,
|
|
Icon: "copilot",
|
|
}
|
|
|
|
// Feature flag names for granular tool variants.
|
|
// When active, consolidated tools are replaced by single-purpose granular tools.
|
|
FeatureFlagIssuesGranular = "issues_granular"
|
|
FeatureFlagPullRequestsGranular = "pull_requests_granular"
|
|
)
|
|
|
|
// HeaderAllowedFeatureFlags returns the feature flags that clients may enable via
|
|
// the X-MCP-Features header. It delegates to AllowedFeatureFlags as the single
|
|
// source of truth.
|
|
func HeaderAllowedFeatureFlags() []string {
|
|
return slices.Clone(AllowedFeatureFlags)
|
|
}
|
|
|
|
var (
|
|
// Remote-only toolsets - these are only available in the remote MCP server
|
|
// but are documented here for consistency and to enable automated documentation.
|
|
ToolsetMetadataCopilotSpaces = inventory.ToolsetMetadata{
|
|
ID: "copilot_spaces",
|
|
Description: "Copilot Spaces tools",
|
|
Icon: "copilot",
|
|
}
|
|
ToolsetMetadataSupportSearch = inventory.ToolsetMetadata{
|
|
ID: "github_support_docs_search",
|
|
Description: "Retrieve documentation to answer GitHub product and support questions. Topics include: GitHub Actions Workflows, Authentication, ...",
|
|
Icon: "book",
|
|
}
|
|
)
|
|
|
|
// AllTools returns all tools with their embedded toolset metadata.
|
|
// Tool functions return ServerTool directly with toolset info.
|
|
func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
|
|
return withCSVOutput([]inventory.ServerTool{
|
|
// Context tools
|
|
GetMe(t),
|
|
GetTeams(t),
|
|
GetTeamMembers(t),
|
|
|
|
// Repository tools
|
|
SearchRepositories(t),
|
|
GetFileContents(t),
|
|
ListCommits(t),
|
|
SearchCode(t),
|
|
SearchCommits(t),
|
|
GetCommit(t),
|
|
GetFileBlame(t),
|
|
ListBranches(t),
|
|
ListTags(t),
|
|
GetTag(t),
|
|
ListReleases(t),
|
|
GetLatestRelease(t),
|
|
GetReleaseByTag(t),
|
|
CreateOrUpdateFile(t),
|
|
CreateRepository(t),
|
|
ForkRepository(t),
|
|
CreateBranch(t),
|
|
PushFiles(t),
|
|
DeleteFile(t),
|
|
ListStarredRepositories(t),
|
|
StarRepository(t),
|
|
UnstarRepository(t),
|
|
ListRepositoryCollaborators(t),
|
|
|
|
// Git tools
|
|
GetRepositoryTree(t),
|
|
|
|
// Issue tools
|
|
IssueRead(t),
|
|
SearchIssues(t),
|
|
ListIssues(t),
|
|
ListIssueTypes(t),
|
|
ListIssueFields(t),
|
|
IssueWrite(t),
|
|
AddIssueComment(t),
|
|
SubIssueWrite(t),
|
|
IssueDependencyRead(t),
|
|
IssueDependencyWrite(t),
|
|
|
|
// User tools
|
|
SearchUsers(t),
|
|
|
|
// Organization tools
|
|
SearchOrgs(t),
|
|
|
|
// Pull request tools
|
|
PullRequestRead(t),
|
|
ListPullRequests(t),
|
|
SearchPullRequests(t),
|
|
MergePullRequest(t),
|
|
UpdatePullRequestBranch(t),
|
|
CreatePullRequest(t),
|
|
UpdatePullRequest(t),
|
|
PullRequestReviewWrite(t),
|
|
AddCommentToPendingReview(t),
|
|
AddReplyToPullRequestComment(t),
|
|
|
|
// Copilot tools
|
|
AssignCopilotToIssue(t),
|
|
RequestCopilotReview(t),
|
|
|
|
// Code quality tools
|
|
GetCodeQualityFinding(t),
|
|
|
|
// Code security tools
|
|
GetCodeScanningAlert(t),
|
|
ListCodeScanningAlerts(t),
|
|
|
|
// Secret protection tools
|
|
GetSecretScanningAlert(t),
|
|
ListSecretScanningAlerts(t),
|
|
|
|
// Dependabot tools
|
|
GetDependabotAlert(t),
|
|
ListDependabotAlerts(t),
|
|
|
|
// Notification tools
|
|
ListNotifications(t),
|
|
GetNotificationDetails(t),
|
|
DismissNotification(t),
|
|
MarkAllNotificationsRead(t),
|
|
ManageNotificationSubscription(t),
|
|
ManageRepositoryNotificationSubscription(t),
|
|
|
|
// Discussion tools
|
|
ListDiscussions(t),
|
|
GetDiscussion(t),
|
|
GetDiscussionComments(t),
|
|
DiscussionCommentWrite(t),
|
|
ListDiscussionCategories(t),
|
|
|
|
// Actions tools
|
|
ActionsList(t),
|
|
ActionsGet(t),
|
|
ActionsRunTrigger(t),
|
|
ActionsGetJobLogs(t),
|
|
|
|
// Security advisories tools
|
|
ListGlobalSecurityAdvisories(t),
|
|
GetGlobalSecurityAdvisory(t),
|
|
ListRepositorySecurityAdvisories(t),
|
|
ListOrgRepositorySecurityAdvisories(t),
|
|
|
|
// Gist tools
|
|
ListGists(t),
|
|
GetGist(t),
|
|
CreateGist(t),
|
|
UpdateGist(t),
|
|
|
|
// Project tools
|
|
ProjectsList(t),
|
|
ProjectsGet(t),
|
|
ProjectsWrite(t),
|
|
|
|
// Label tools
|
|
GetLabel(t),
|
|
GetLabelForLabelsToolset(t),
|
|
ListLabels(t),
|
|
LabelWrite(t),
|
|
|
|
// UI tools (insiders only)
|
|
UIGet(t),
|
|
|
|
// Granular issue tools (feature-flagged, replace consolidated issue_write/sub_issue_write)
|
|
GranularCreateIssue(t),
|
|
GranularUpdateIssueTitle(t),
|
|
GranularUpdateIssueBody(t),
|
|
GranularUpdateIssueAssignees(t),
|
|
GranularUpdateIssueLabels(t),
|
|
GranularUpdateIssueMilestone(t),
|
|
GranularUpdateIssueType(t),
|
|
GranularUpdateIssueState(t),
|
|
GranularAddSubIssue(t),
|
|
GranularRemoveSubIssue(t),
|
|
GranularReprioritizeSubIssue(t),
|
|
GranularSetIssueFields(t),
|
|
GranularAddIssueReaction(t),
|
|
GranularAddIssueCommentReaction(t),
|
|
|
|
// Granular pull request tools (feature-flagged, replace consolidated update_pull_request/pull_request_review_write)
|
|
GranularUpdatePullRequestTitle(t),
|
|
GranularUpdatePullRequestBody(t),
|
|
GranularUpdatePullRequestState(t),
|
|
GranularUpdatePullRequestDraftState(t),
|
|
GranularRequestPullRequestReviewers(t),
|
|
GranularCreatePullRequestReview(t),
|
|
GranularSubmitPendingPullRequestReview(t),
|
|
GranularDeletePendingPullRequestReview(t),
|
|
GranularAddPullRequestReviewComment(t),
|
|
GranularResolveReviewThread(t),
|
|
GranularUnresolveReviewThread(t),
|
|
GranularAddPullRequestReviewCommentReaction(t),
|
|
})
|
|
}
|
|
|
|
// ToBoolPtr converts a bool to a *bool pointer.
|
|
func ToBoolPtr(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
// ToStringPtr converts a string to a *string pointer.
|
|
// Returns nil if the string is empty.
|
|
func ToStringPtr(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
// GenerateToolsetsHelp generates the help text for the toolsets flag
|
|
func GenerateToolsetsHelp() string {
|
|
// Get toolset group to derive defaults and available toolsets
|
|
// Build() can only fail if WithTools specifies invalid tools - not used here
|
|
r, _ := NewInventory(stubTranslator).Build()
|
|
|
|
// Format default tools from metadata using strings.Builder
|
|
var defaultBuf strings.Builder
|
|
defaultIDs := r.DefaultToolsetIDs()
|
|
for i, id := range defaultIDs {
|
|
if i > 0 {
|
|
defaultBuf.WriteString(", ")
|
|
}
|
|
defaultBuf.WriteString(string(id))
|
|
}
|
|
|
|
// Get all available toolsets (excludes context for display)
|
|
allToolsets := r.AvailableToolsets("context")
|
|
var availableBuf strings.Builder
|
|
const maxLineLength = 70
|
|
currentLine := ""
|
|
|
|
for i, toolset := range allToolsets {
|
|
id := string(toolset.ID)
|
|
switch {
|
|
case i == 0:
|
|
currentLine = id
|
|
case len(currentLine)+len(id)+2 <= maxLineLength:
|
|
currentLine += ", " + id
|
|
default:
|
|
if availableBuf.Len() > 0 {
|
|
availableBuf.WriteString(",\n\t ")
|
|
}
|
|
availableBuf.WriteString(currentLine)
|
|
currentLine = id
|
|
}
|
|
}
|
|
if currentLine != "" {
|
|
if availableBuf.Len() > 0 {
|
|
availableBuf.WriteString(",\n\t ")
|
|
}
|
|
availableBuf.WriteString(currentLine)
|
|
}
|
|
|
|
// Build the complete help text using strings.Builder
|
|
var buf strings.Builder
|
|
buf.WriteString("Comma-separated list of tool groups to enable (no spaces).\n")
|
|
buf.WriteString("Available: ")
|
|
buf.WriteString(availableBuf.String())
|
|
buf.WriteString("\n")
|
|
buf.WriteString("Special toolset keywords:\n")
|
|
buf.WriteString(" - all: Enables all available toolsets\n")
|
|
buf.WriteString(" - default: Enables the default toolset configuration of:\n\t ")
|
|
buf.WriteString(defaultBuf.String())
|
|
buf.WriteString("\n")
|
|
buf.WriteString("Examples:\n")
|
|
buf.WriteString(" - --toolsets=actions,gists,notifications\n")
|
|
buf.WriteString(" - Default + additional: --toolsets=default,actions,gists\n")
|
|
buf.WriteString(" - All tools: --toolsets=all")
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// stubTranslator is a passthrough translator for cases where we need an Inventory
|
|
// but don't need actual translations (e.g., getting toolset IDs for CLI help).
|
|
func stubTranslator(_, fallback string) string { return fallback }
|
|
|
|
// AddDefaultToolset removes the default toolset and expands it to the actual default toolset IDs
|
|
func AddDefaultToolset(result []string) []string {
|
|
hasDefault := false
|
|
seen := make(map[string]bool)
|
|
for _, toolset := range result {
|
|
seen[toolset] = true
|
|
if toolset == string(ToolsetMetadataDefault.ID) {
|
|
hasDefault = true
|
|
}
|
|
}
|
|
|
|
// Only expand if "default" keyword was found
|
|
if !hasDefault {
|
|
return result
|
|
}
|
|
|
|
result = RemoveToolset(result, string(ToolsetMetadataDefault.ID))
|
|
|
|
// Get default toolset IDs from the Inventory
|
|
// Build() can only fail if WithTools specifies invalid tools - not used here
|
|
r, _ := NewInventory(stubTranslator).Build()
|
|
for _, id := range r.DefaultToolsetIDs() {
|
|
if !seen[string(id)] {
|
|
result = append(result, string(id))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func RemoveToolset(tools []string, toRemove string) []string {
|
|
result := make([]string, 0, len(tools))
|
|
for _, tool := range tools {
|
|
if tool != toRemove {
|
|
result = append(result, tool)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func ContainsToolset(tools []string, toCheck string) bool {
|
|
return slices.Contains(tools, toCheck)
|
|
}
|
|
|
|
// CleanTools cleans tool names by removing duplicates and trimming whitespace.
|
|
// Validation of tool existence is done during registration.
|
|
func CleanTools(toolNames []string) []string {
|
|
seen := make(map[string]bool)
|
|
result := make([]string, 0, len(toolNames))
|
|
|
|
// Remove duplicates and trim whitespace
|
|
for _, tool := range toolNames {
|
|
trimmed := strings.TrimSpace(tool)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if !seen[trimmed] {
|
|
seen[trimmed] = true
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetDefaultToolsetIDs returns the IDs of toolsets marked as Default.
|
|
// This is a convenience function that builds an inventory to determine defaults.
|
|
func GetDefaultToolsetIDs() []string {
|
|
// Build() can only fail if WithTools specifies invalid tools - not used here
|
|
r, _ := NewInventory(stubTranslator).Build()
|
|
ids := r.DefaultToolsetIDs()
|
|
result := make([]string, len(ids))
|
|
for i, id := range ids {
|
|
result[i] = string(id)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// RemoteOnlyToolsets returns toolset metadata for toolsets that are only
|
|
// available in the remote MCP server. These are documented but not registered
|
|
// in the local server.
|
|
func RemoteOnlyToolsets() []inventory.ToolsetMetadata {
|
|
return []inventory.ToolsetMetadata{
|
|
ToolsetMetadataCopilotSpaces,
|
|
ToolsetMetadataSupportSearch,
|
|
}
|
|
}
|