Skip to content

Commit 9bcf526

Browse files
feat(toolsets): add AvailableToolsets() with exclude filter
- Add AvailableToolsets() method that returns toolsets with actual tools - Support variadic exclude parameter for filtering out specific toolsets - Simplifies doc generation by removing manual skip logic - Naturally excludes empty toolsets (like 'dynamic') without special cases
1 parent 74d2c56 commit 9bcf526

File tree

2 files changed

+61
-50
lines changed

2 files changed

+61
-50
lines changed

cmd/github-mcp-server/generate_docs.go

Lines changed: 32 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -116,19 +116,10 @@ func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
116116
// Add the context toolset row with custom description (strongly recommended)
117117
buf.WriteString("| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n")
118118

119-
// AllTools() is sorted by toolset ID then tool name.
120-
// We iterate once, collecting unique toolsets (skipping context which has custom description above).
121-
tools := tsg.AllTools()
122-
var lastToolsetID toolsets.ToolsetID
123-
for _, tool := range tools {
124-
if tool.Toolset.ID != lastToolsetID {
125-
lastToolsetID = tool.Toolset.ID
126-
// Skip context (handled above with custom description)
127-
if lastToolsetID == "context" {
128-
continue
129-
}
130-
fmt.Fprintf(&buf, "| `%s` | %s |\n", lastToolsetID, tool.Toolset.Description)
131-
}
119+
// AvailableToolsets() returns toolsets that have tools, sorted by ID
120+
// Exclude context (custom description above) and dynamic (internal only)
121+
for _, ts := range tsg.AvailableToolsets("context", "dynamic") {
122+
fmt.Fprintf(&buf, "| `%s` | %s |\n", ts.ID, ts.Description)
132123
}
133124

134125
return strings.TrimSuffix(buf.String(), "\n")
@@ -311,43 +302,34 @@ func generateRemoteToolsetsDoc() string {
311302
// Add "all" toolset first (special case)
312303
buf.WriteString("| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |\n")
313304

314-
// AllTools() is sorted by toolset ID then tool name.
315-
// We iterate once, collecting unique toolsets (skipping context which is handled separately).
316-
tools := tsg.AllTools()
317-
var lastToolsetID toolsets.ToolsetID
318-
for _, tool := range tools {
319-
if tool.Toolset.ID != lastToolsetID {
320-
lastToolsetID = tool.Toolset.ID
321-
idStr := string(lastToolsetID)
322-
// Skip context toolset (handled separately)
323-
if idStr == "context" {
324-
continue
325-
}
326-
327-
formattedName := formatToolsetName(idStr)
328-
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
329-
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
330-
331-
// Create install config JSON (URL encoded)
332-
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
333-
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
334-
335-
// Fix URL encoding to use %20 instead of + for spaces
336-
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
337-
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
338-
339-
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
340-
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
341-
342-
fmt.Fprintf(&buf, "| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
343-
formattedName,
344-
tool.Toolset.Description,
345-
apiURL,
346-
installLink,
347-
fmt.Sprintf("[read-only](%s)", readonlyURL),
348-
readonlyInstallLink,
349-
)
350-
}
305+
// AvailableToolsets() returns toolsets that have tools, sorted by ID
306+
// Exclude context (handled separately) and dynamic (internal only)
307+
for _, ts := range tsg.AvailableToolsets("context", "dynamic") {
308+
idStr := string(ts.ID)
309+
310+
formattedName := formatToolsetName(idStr)
311+
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
312+
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
313+
314+
// Create install config JSON (URL encoded)
315+
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
316+
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
317+
318+
// Fix URL encoding to use %20 instead of + for spaces
319+
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
320+
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
321+
322+
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
323+
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
324+
325+
fmt.Fprintf(&buf, "| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
326+
formattedName,
327+
ts.Description,
328+
apiURL,
329+
installLink,
330+
fmt.Sprintf("[read-only](%s)", readonlyURL),
331+
readonlyInstallLink,
332+
)
351333
}
352334

353335
return strings.TrimSuffix(buf.String(), "\n")

pkg/toolsets/toolsets.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,3 +767,32 @@ func (tg *ToolsetGroup) AllTools() []ServerTool {
767767

768768
return result
769769
}
770+
771+
// AvailableToolsets returns the unique toolsets that have tools, in sorted order.
772+
// This is the ordered intersection of toolsets with reality - only toolsets that
773+
// actually contain tools are returned, sorted by toolset ID.
774+
// Optional exclude parameter filters out specific toolset IDs from the result.
775+
func (tg *ToolsetGroup) AvailableToolsets(exclude ...ToolsetID) []ToolsetMetadata {
776+
tools := tg.AllTools()
777+
if len(tools) == 0 {
778+
return nil
779+
}
780+
781+
// Build exclude set for O(1) lookup
782+
excludeSet := make(map[ToolsetID]bool, len(exclude))
783+
for _, id := range exclude {
784+
excludeSet[id] = true
785+
}
786+
787+
var result []ToolsetMetadata
788+
var lastID ToolsetID
789+
for _, tool := range tools {
790+
if tool.Toolset.ID != lastID {
791+
lastID = tool.Toolset.ID
792+
if !excludeSet[lastID] {
793+
result = append(result, tool.Toolset)
794+
}
795+
}
796+
}
797+
return result
798+
}

0 commit comments

Comments
 (0)