diff --git a/go.mod b/go.mod index 661778fc3..91f7e771b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e422a548c..c35b0f23f 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/pkg/github/tools.go b/pkg/github/tools.go index f21a9ae5b..b2c645553 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -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", } ) @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -304,9 +365,11 @@ 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)), @@ -314,6 +377,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG ) gists := toolsets.NewToolset(ToolsetMetadataGists.ID, ToolsetMetadataGists.Description). + SetIcons(ToolsetMetadataGists.ToolsetIcon()). AddReadTools( toolsets.NewServerTool(ListGists(getClient, t)), toolsets.NewServerTool(GetGist(getClient, t)), @@ -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)), @@ -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)), ). @@ -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)), @@ -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)), diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go index d96b5fb50..1ce4223d3 100644 --- a/pkg/toolsets/toolsets.go +++ b/pkg/toolsets/toolsets.go @@ -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) { @@ -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 @@ -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...) @@ -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 @@ -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 {