Files
github--github-mcp-server/pkg/github/tools.go
T
Matt Holloway 4e8eb81dac MCP Apps with extra functionality (#1974)
* PoC full flow (hello world example)

* add avatar resource domain

* add postmessage logic and richer UI

* add create issue ui

* update ui for issue creatioon

* fix

* ignore banner

* update docs after rebase

* update toolsnap for get_me

* new UI changes

* update docs

* update workflows that need ui build

* add UI diff

* fix build ui step for windows runners to use git bash

* fix UI diff

* refactor issue creation UI

* add AvatarWithFallback component and update UserCard to use it; enhance CreateIssueApp to manage existing issue data

* fix formatting of button labels

* add create pull request functionality with UI support and insiders

* update docs

* add test for insiders mode handling in ServerTool schema

* remove `show_ui` param for now

* make insiders mode metadata stripping generic

* remove ui diff

* fix CI

* remove redundant mention of old app name

* add node types to fix ide issues for ts code

* remove unused TriangleDownIcon import

* update @primer/behaviors and electron-to-chromium versions in package-lock.json

* add check to ensure base and head are not the same when creating a new PR

* remove old show_ui

* fix gitignore for dist so builds dont break

* add tests for insiders mode handling and metadata stripping in ServerTool

* remove unused state and components from CreatePRApp

* fix ui build

* update docker build to fix npm issue

* remove reference to show_ui

* allow insiders to work for non-ui features

* formalise insiders inventory support

* update docs

* fix overflow issues and replace pull request dropdown with matching UI from dotcom

* fix createpullrequest test

* consolidate fetching tools under `ui_get` tool to remove toolset deps

* fix issue data prefill in issue_write form

* fix link component when updating issue

* fix avatar URL

* fix broken issue update logic

* remove dbg

* fix for new GetFlags

* revert to original required fields for create_pull_request

* fix for UI form submission

* Simplify MCP App UIs for basic branch

Remove advanced features to be kept in mcp-ui-apps-advanced:
- Strip labels, assignees, milestones, issue types, repo picker from issue-write
- Strip repo picker, branch selectors from pr-write
- Delete ui_get tool (ui_tools.go, ui_tools_test.go, ui_get.snap)
- Remove UIGet registration from tools.go

Basic forms retain: title, body, submit with _ui_submitted,
draft/regular split button (PR), MarkdownEditor, and SuccessView.

* Fix header spacing in issue-write and pr-write UIs

Add proper spacing between icon, title text, and repo name in the
header bar for both issue-write and create-pull-request forms.

* fix UI spacing

* Revert "Simplify MCP App UIs for basic branch"

This reverts commit 24174b91e222e45b47913ff6b760db809f48660b.

* Undo dependency downgrades in ui/package-lock.json

* Update ui/src/apps/pr-write/App.tsx

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

* Update ui/src/apps/issue-write/App.tsx

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

* Implement pagination for uiGetBranches (#2012)

* Initial plan

* Implement pagination for uiGetBranches function

Co-authored-by: mattdholloway <918573+mattdholloway@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mattdholloway <918573+mattdholloway@users.noreply.github.com>

* update to new insiders feature flag func

* ensure transient state is reset on successive tool calls

* Mark ui_get as app-only visibility

ui_get backs only the MCP App views and has no business in the agent's
tool list. Per the MCP Apps 2026-01-26 spec, omitting _meta.ui.visibility
defaults to ["model","app"], which exposes the tool to the model. Declare
visibility ["app"] so the host hides it from tools/list while the views can
still invoke it via tools/call.

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

* Update ui_get toolsnap for app-only visibility

Regenerated via UPDATE_TOOLSNAPS to capture the new _meta.ui.visibility.

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

* Assert ui_get declares app-only visibility

Locks in the _meta.ui.visibility ["app"] contract so a future edit can't
silently re-expose the UI data tool to the model.

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

* Add ui_get to insiders feature docs

Regenerated docs/feature-flags.md and docs/insiders-features.md to include
the ui_get tool entry.

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

* Address ui_get review feedback

- Paginate the labels GraphQL query (cursor-based) so repos with more than
  100 labels return a complete list instead of silently truncating.
- Emit an empty due_on for milestones without a due date instead of
  formatting the zero time as "0001-01-01".
- Use NewGitHubAPIErrorResponse in uiGetIssueTypes to preserve GitHub
  response context, matching the other REST-backed methods.
- Extend tests to cover the labels (GraphQL), milestones (including the
  no-due-date case) and issue_types methods, plus the issue_types error path.

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

* Fix repo reset and stale base-branch in MCP App views

- Re-initialize selectedRepo from toolInput inside the reset-on-invocation
  effect instead of a separate effect. The two effects both depended on
  toolInput and ran in declaration order, so the reset wiped the just-
  initialized repo and the picker never reflected the invocation's owner/repo.
- Set the default base branch with a functional update in pr-write so a base
  prefilled from toolInput.base (or chosen by the user) isn't overwritten by
  a stale baseBranch value captured before the branches request resolved.

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

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix issue-write repo owner mapping and clear stale UI state on reset

- issue-write: derive owner/name from full_name since search_repositories
  minimal output omits the owner object (mirrors pr-write)
- pr-write/issue-write: clear available branch/label/assignee/milestone/type
  lists and filters in the toolInput reset effect so prefill effects can't
  match against the previous repo's stale data

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

* Merge remote-tracking branch 'origin/main' into mcp-ui-apps-advanced

* feat: add pull request editing functionality with reviewers support

* feat: implement interactive form handling for issue and pull request creation and updates

* Close response body per page in ui_get pagination loops

Avoids leaking HTTP connections when paging through assignees,
milestones, branches, collaborators, and teams.

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

* Cache pr-edit.html in build-ui action

The build-ui cache only saved get-me/issue-write/pr-write HTML, so once a
cache entry was stored it restored an incomplete ui_dist on later runs and
skipped the rebuild, leaving pr-edit.html absent and panicking the tests.
Add pr-edit.html to the cached paths and bump the cache key to v2 to evict
the incomplete entries.

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

---------

Co-authored-by: tommaso-moro <tommaso-moro@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 16:45:16 +01:00

493 lines
14 KiB
Go

package github
import (
"context"
"slices"
"strings"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
)
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",
}
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),
LegacyListIssues(t),
ListIssueTypes(t),
ListIssueFields(t),
IssueWrite(t),
LegacyIssueWrite(t),
AddIssueComment(t),
SubIssueWrite(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 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),
// 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),
})
}
// 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,
}
}