Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/modelcontextprotocol/go-sdk v1.2.0-pre.1
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand Down Expand Up @@ -55,8 +57,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/modelcontextprotocol/go-sdk v1.2.0-pre.1 h1:14+JrlEIFvUmbu5+iJzWPLk8CkpvegfKr42oXyjp3O4=
github.com/modelcontextprotocol/go-sdk v1.2.0-pre.1/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
Expand Down
70 changes: 69 additions & 1 deletion pkg/github/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,96 +21,144 @@ type GetGQLClientFn func(context.Context) (*githubv4.Client, error)
type ToolsetMetadata struct {
ID string
Description string
// Icon is the name of the Octicon to use for this toolset (without size suffix, e.g., "repo" not "repo-16")
Icon string
}

// OcticonURL returns the URL for an Octicon SVG icon. Size should be 16 or 24.
func OcticonURL(name string, size int) string {
return fmt.Sprintf("https://raw.githubusercontent.com/primer/octicons/main/icons/%s-%d.svg", name, size)
}

// ToolsetIcon creates an mcp.Icon for the toolset using Octicons
func (tm ToolsetMetadata) ToolsetIcon() []mcp.Icon {
if tm.Icon == "" {
return nil
}
return []mcp.Icon{
{
Source: OcticonURL(tm.Icon, 16),
MIMEType: "image/svg+xml",
Sizes: []string{"16x16"},
},
{
Source: OcticonURL(tm.Icon, 24),
MIMEType: "image/svg+xml",
Sizes: []string{"24x24"},
},
}
}

var (
ToolsetMetadataAll = ToolsetMetadata{
ID: "all",
Description: "Special toolset that enables all available toolsets",
Icon: "apps",
}
ToolsetMetadataDefault = 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 = ToolsetMetadata{
ID: "context",
Description: "Tools that provide context about the current user and GitHub context you are operating in",
Icon: "person",
}
ToolsetMetadataRepos = ToolsetMetadata{
ID: "repos",
Description: "GitHub Repository related tools",
Icon: "repo",
}
ToolsetMetadataGit = ToolsetMetadata{
ID: "git",
Description: "GitHub Git API related tools for low-level Git operations",
Icon: "git-branch",
}
ToolsetMetadataIssues = ToolsetMetadata{
ID: "issues",
Description: "GitHub Issues related tools",
Icon: "issue-opened",
}
ToolsetMetadataPullRequests = ToolsetMetadata{
ID: "pull_requests",
Description: "GitHub Pull Request related tools",
Icon: "git-pull-request",
}
ToolsetMetadataUsers = ToolsetMetadata{
ID: "users",
Description: "GitHub User related tools",
Icon: "people",
}
ToolsetMetadataOrgs = ToolsetMetadata{
ID: "orgs",
Description: "GitHub Organization related tools",
Icon: "organization",
}
ToolsetMetadataActions = ToolsetMetadata{
ID: "actions",
Description: "GitHub Actions workflows and CI/CD operations",
Icon: "play",
}
ToolsetMetadataCodeSecurity = ToolsetMetadata{
ID: "code_security",
Description: "Code security related tools, such as GitHub Code Scanning",
Icon: "codescan",
}
ToolsetMetadataSecretProtection = ToolsetMetadata{
ID: "secret_protection",
Description: "Secret protection related tools, such as GitHub Secret Scanning",
Icon: "key",
}
ToolsetMetadataDependabot = ToolsetMetadata{
ID: "dependabot",
Description: "Dependabot tools",
Icon: "dependabot",
}
ToolsetMetadataNotifications = ToolsetMetadata{
ID: "notifications",
Description: "GitHub Notifications related tools",
Icon: "bell",
}
ToolsetMetadataExperiments = ToolsetMetadata{
ID: "experiments",
Description: "Experimental features that are not considered stable yet",
Icon: "beaker",
}
ToolsetMetadataDiscussions = ToolsetMetadata{
ID: "discussions",
Description: "GitHub Discussions related tools",
Icon: "comment-discussion",
}
ToolsetMetadataGists = ToolsetMetadata{
ID: "gists",
Description: "GitHub Gist related tools",
Icon: "code",
}
ToolsetMetadataSecurityAdvisories = ToolsetMetadata{
ID: "security_advisories",
Description: "Security advisories related tools",
Icon: "shield",
}
ToolsetMetadataProjects = ToolsetMetadata{
ID: "projects",
Description: "GitHub Projects related tools",
Icon: "project",
}
ToolsetMetadataStargazers = ToolsetMetadata{
ID: "stargazers",
Description: "GitHub Stargazers related tools",
Icon: "star",
}
ToolsetMetadataDynamic = ToolsetMetadata{
ID: "dynamic",
Description: "Discover GitHub MCP tools that can help achieve tasks by enabling additional sets of tools, you can control the enablement of any toolset to access its tools when this toolset is enabled.",
Icon: "tools",
}
ToolsetLabels = ToolsetMetadata{
ID: "labels",
Description: "GitHub Labels related tools",
Icon: "tag",
}
)

Expand Down Expand Up @@ -166,6 +214,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
// Define all available features with their default state (disabled)
// Create toolsets
repos := toolsets.NewToolset(ToolsetMetadataRepos.ID, ToolsetMetadataRepos.Description).
SetIcons(ToolsetMetadataRepos.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(SearchRepositories(getClient, t)),
toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)),
Expand Down Expand Up @@ -195,10 +244,12 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)),
)
git := toolsets.NewToolset(ToolsetMetadataGit.ID, ToolsetMetadataGit.Description).
SetIcons(ToolsetMetadataGit.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(GetRepositoryTree(getClient, t)),
)
issues := toolsets.NewToolset(ToolsetMetadataIssues.ID, ToolsetMetadataIssues.Description).
SetIcons(ToolsetMetadataIssues.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(IssueRead(getClient, getGQLClient, cache, t, flags)),
toolsets.NewServerTool(SearchIssues(getClient, t)),
Expand All @@ -216,14 +267,17 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)),
)
users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description).
SetIcons(ToolsetMetadataUsers.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(SearchUsers(getClient, t)),
)
orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description).
SetIcons(ToolsetMetadataOrgs.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(SearchOrgs(getClient, t)),
)
pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description).
SetIcons(ToolsetMetadataPullRequests.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(PullRequestRead(getClient, cache, t, flags)),
toolsets.NewServerTool(ListPullRequests(getClient, t)),
Expand All @@ -240,22 +294,26 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)),
)
codeSecurity := toolsets.NewToolset(ToolsetMetadataCodeSecurity.ID, ToolsetMetadataCodeSecurity.Description).
SetIcons(ToolsetMetadataCodeSecurity.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)),
toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)),
)
secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description).
SetIcons(ToolsetMetadataSecretProtection.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)),
toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)),
)
dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description).
SetIcons(ToolsetMetadataDependabot.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(GetDependabotAlert(getClient, t)),
toolsets.NewServerTool(ListDependabotAlerts(getClient, t)),
)

notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description).
SetIcons(ToolsetMetadataNotifications.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListNotifications(getClient, t)),
toolsets.NewServerTool(GetNotificationDetails(getClient, t)),
Expand All @@ -268,6 +326,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)

discussions := toolsets.NewToolset(ToolsetMetadataDiscussions.ID, ToolsetMetadataDiscussions.Description).
SetIcons(ToolsetMetadataDiscussions.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListDiscussions(getGQLClient, t)),
toolsets.NewServerTool(GetDiscussion(getGQLClient, t)),
Expand All @@ -276,6 +335,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)

actions := toolsets.NewToolset(ToolsetMetadataActions.ID, ToolsetMetadataActions.Description).
SetIcons(ToolsetMetadataActions.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListWorkflows(getClient, t)),
toolsets.NewServerTool(ListWorkflowRuns(getClient, t)),
Expand All @@ -296,6 +356,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)

securityAdvisories := toolsets.NewToolset(ToolsetMetadataSecurityAdvisories.ID, ToolsetMetadataSecurityAdvisories.Description).
SetIcons(ToolsetMetadataSecurityAdvisories.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)),
toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)),
Expand All @@ -304,16 +365,19 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)

// // Keep experiments alive so the system doesn't error out when it's always enabled
experiments := toolsets.NewToolset(ToolsetMetadataExperiments.ID, ToolsetMetadataExperiments.Description)
experiments := toolsets.NewToolset(ToolsetMetadataExperiments.ID, ToolsetMetadataExperiments.Description).
SetIcons(ToolsetMetadataExperiments.ToolsetIcon())

contextTools := toolsets.NewToolset(ToolsetMetadataContext.ID, ToolsetMetadataContext.Description).
SetIcons(ToolsetMetadataContext.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(GetMe(getClient, t)),
toolsets.NewServerTool(GetTeams(getClient, getGQLClient, t)),
toolsets.NewServerTool(GetTeamMembers(getGQLClient, t)),
)

gists := toolsets.NewToolset(ToolsetMetadataGists.ID, ToolsetMetadataGists.Description).
SetIcons(ToolsetMetadataGists.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListGists(getClient, t)),
toolsets.NewServerTool(GetGist(getClient, t)),
Expand All @@ -324,6 +388,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)

projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description).
SetIcons(ToolsetMetadataProjects.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListProjects(getClient, t)),
toolsets.NewServerTool(GetProject(getClient, t)),
Expand All @@ -338,6 +403,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(UpdateProjectItem(getClient, t)),
)
stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description).
SetIcons(ToolsetMetadataStargazers.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListStarredRepositories(getClient, t)),
).
Expand All @@ -346,6 +412,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(UnstarRepository(getClient, t)),
)
labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description).
SetIcons(ToolsetLabels.ToolsetIcon()).
AddReadTools(
// get
toolsets.NewServerTool(GetLabel(getGQLClient, t)),
Expand Down Expand Up @@ -390,6 +457,7 @@ func InitDynamicToolset(s *mcp.Server, tsg *toolsets.ToolsetGroup, t translation
// Create a new dynamic toolset
// Need to add the dynamic toolset last so it can be used to enable other toolsets
dynamicToolSelection := toolsets.NewToolset(ToolsetMetadataDynamic.ID, ToolsetMetadataDynamic.Description).
SetIcons(ToolsetMetadataDynamic.ToolsetIcon()).
AddReadTools(
toolsets.NewServerTool(ListAvailableToolsets(tsg, t)),
toolsets.NewServerTool(GetToolsetsTools(tsg, t)),
Expand Down
36 changes: 30 additions & 6 deletions pkg/toolsets/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ type ServerTool struct {
RegisterFunc func(s *mcp.Server)
}

// WithIcons sets the icons on the tool and returns the modified ServerTool.
func (st ServerTool) WithIcons(icons []mcp.Icon) ServerTool {
st.Tool.Icons = icons
return st
}

func NewServerTool[In any, Out any](tool mcp.Tool, handler mcp.ToolHandlerFor[In, Out]) ServerTool {
return ServerTool{Tool: tool, RegisterFunc: func(s *mcp.Server) {
th := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Expand Down Expand Up @@ -86,6 +92,8 @@ type Toolset struct {
readOnly bool
writeTools []ServerTool
readTools []ServerTool
// icons are optional icons to apply to all tools in this toolset (if not already set)
icons []mcp.Icon
// resources are not tools, but the community seems to be moving towards namespaces as a broader concept
// and in order to have multiple servers running concurrently, we want to avoid overlapping resources too.
resourceTemplates []ServerResourceTemplate
Expand Down Expand Up @@ -170,10 +178,11 @@ func (t *Toolset) SetReadOnly() {

func (t *Toolset) AddWriteTools(tools ...ServerTool) *Toolset {
// Silently ignore if the toolset is read-only to avoid any breach of that contract
for _, tool := range tools {
if tool.Tool.Annotations.ReadOnlyHint {
panic(fmt.Sprintf("tool (%s) is incorrectly annotated as read-only", tool.Tool.Name))
for i := range tools {
if tools[i].Tool.Annotations.ReadOnlyHint {
panic(fmt.Sprintf("tool (%s) is incorrectly annotated as read-only", tools[i].Tool.Name))
}
t.applyIcons(&tools[i])
}
if !t.readOnly {
t.writeTools = append(t.writeTools, tools...)
Expand All @@ -182,10 +191,11 @@ func (t *Toolset) AddWriteTools(tools ...ServerTool) *Toolset {
}

func (t *Toolset) AddReadTools(tools ...ServerTool) *Toolset {
for _, tool := range tools {
if !tool.Tool.Annotations.ReadOnlyHint {
panic(fmt.Sprintf("tool (%s) must be annotated as read-only", tool.Tool.Name))
for i := range tools {
if !tools[i].Tool.Annotations.ReadOnlyHint {
panic(fmt.Sprintf("tool (%s) must be annotated as read-only", tools[i].Tool.Name))
}
t.applyIcons(&tools[i])
}
t.readTools = append(t.readTools, tools...)
return t
Expand Down Expand Up @@ -229,6 +239,20 @@ func NewToolset(name string, description string) *Toolset {
}
}

// SetIcons sets the default icons for all tools in this toolset.
// Icons will be applied to tools that don't already have icons set.
func (t *Toolset) SetIcons(icons []mcp.Icon) *Toolset {
t.icons = icons
return t
}

// applyIcons applies the toolset's icons to a tool if it doesn't already have icons.
func (t *Toolset) applyIcons(tool *ServerTool) {
if len(tool.Tool.Icons) == 0 && len(t.icons) > 0 {
tool.Tool.Icons = t.icons
}
}

func (tg *ToolsetGroup) IsEnabled(name string) bool {
// If everythingOn is true, all features are enabled
if tg.everythingOn {
Expand Down
Loading