Files
github--github-mcp-server/pkg/github/tools.go
Sam Morrow 8ec62491c6 Add confirmed repository deletion tool (#3076)
* feat(repos): add confirmed repository deletion

Add a destructive delete_repository tool that requires an exact owner/repo confirmation through multi-round-trip elicitation. Gate the tool to MCP protocol 2026-07-28 and newer across local and remote transports.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* refactor(inventory): generalize tool availability guards

Gate protocol-restricted tools on required elicitation capabilities and enforce direct calls inside the registered handler so SDK result finalization remains intact.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* feat(http): protect MRTR request state

Seal repository deletion targets for self-hosted HTTP with a stable AES-256-GCM key. Hide only delete_repository when no key is configured and expose an optional sealer interface for remote integrators.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(repos): expire deletion confirmations

Bind sealed repository deletion state to the immutable repository ID and a ten-minute expiry. Re-check identity before deletion so replay cannot affect a recreated repository.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(http): preserve tool and scope restrictions

Apply static allowlists before removing unavailable tools and fail closed on invalid configured tool names. Model independent OAuth requirements as conjunctive groups so repository deletion requires both delete_repo and repo.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(repos): require protected confirmation state

Give stdio a process-local request-state sealer and make deletion fail closed without one. Preserve legacy any-of OAuth behavior globally while documenting and enforcing delete_repository's conjunctive delete_repo and repo requirements.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(oauth): request repository deletion scope

Include delete_repo in the supported OAuth scope set used by stdio login, HTTP protected-resource metadata, and tool filtering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(oauth): require deletion scope opt-in

Keep delete_repo in protected-resource discovery for step-up authorization while excluding it from the default stdio OAuth grant.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* refactor(oauth): derive scope sets from catalog

Generate protected-resource supported scopes and the lower-risk default OAuth grant from one canonical scope definition list.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* refactor(scopes): own OAuth scope catalog

Move supported and default OAuth scope policy into pkg/scopes so protected-resource metadata and stdio grants derive from the scope domain package.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

* fix(scopes): require workflow scope opt-in

Keep workflow and codespace in protected-resource discovery while excluding both from the default OAuth grant alongside delete_repo.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8

---------

Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
2026-08-18 14:50:38 +02:00

547 lines
16 KiB
Go

package github
import (
"context"
"slices"
"strings"
"github.com/google/go-github/v89/github"
"github.com/shurcooL/githubv4"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
)
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",
}
// ToolsetMetadataCopilotIssueIntents is a non-default toolset that gates the
// opt-in intent-aware Copilot issue assignment tool. Kept out of the default
// configuration so its inputs (rationale, confidence, is_suggestion) do not
// add schema bloat to the default tool surface.
ToolsetMetadataCopilotIssueIntents = inventory.ToolsetMetadata{
ID: "copilot_issue_intents",
Description: "Opt-in Copilot issue assignment tools that carry intent metadata (rationale, confidence, suggestion)",
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",
}
)
// ToolOption configures how tools are built. Options carry deployment
// capabilities that are known when the inventory is constructed, so a tool's
// description and its behaviour are decided from the same value and cannot
// drift apart.
type ToolOption func(*toolConfig)
type toolConfig struct {
// hostType is the deployment the tools will talk to. The zero value is
// dotcom, which is also what an empty GITHUB_HOST resolves to.
hostType utils.HostType
}
// WithHost tells the tools which deployment they will talk to, so those with
// host-specific capabilities can adapt. Derive it from utils.ParseHostType.
func WithHost(h utils.HostType) ToolOption {
return func(c *toolConfig) { c.hostType = h }
}
func newToolConfig(opts []ToolOption) toolConfig {
var cfg toolConfig
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// AllTools returns all tools with their embedded toolset metadata.
// Tool functions return ServerTool directly with toolset info.
func AllTools(t translations.TranslationHelperFunc, opts ...ToolOption) []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),
DeleteRepository(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, opts...),
ListIssues(t),
ListIssueTypes(t),
ListIssueFields(t),
IssueWrite(t),
AddIssueComment(t),
SubIssueWrite(t),
IssueDependencyRead(t),
IssueDependencyWrite(t),
FindDuplicate(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),
// Copilot issue intents (non-default, opt-in)
AssignCopilotToIssueWithIntent(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,
}
}