Skip to content

Commit 74d2c56

Browse files
refactor(generate_docs): use strings.Builder and AllTools() iteration
- Replace slice joining with strings.Builder for all doc generation - Iterate AllTools() directly instead of ToolsetIDs()/ToolsForToolset() - Removes need for special 'dynamic' toolset handling (no tools = no output) - Context toolset still explicitly handled for custom description - Consistent pattern across generateToolsetsDoc, generateToolsDoc, generateRemoteToolsetsDoc, and generateDeprecatedAliasesTable
1 parent b13d9fe commit 74d2c56

File tree

2 files changed

+120
-107
lines changed

2 files changed

+120
-107
lines changed

cmd/github-mcp-server/generate_docs.go

Lines changed: 120 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -107,64 +107,73 @@ func generateRemoteServerDocs(docsPath string) error {
107107
}
108108

109109
func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
110-
var lines []string
110+
var buf strings.Builder
111111

112112
// Add table header and separator
113-
lines = append(lines, "| Toolset | Description |")
114-
lines = append(lines, "| ----------------------- | ------------------------------------------------------------- |")
115-
116-
// Add the context toolset row (handled separately in README)
117-
lines = append(lines, "| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |")
118-
119-
// Get toolset IDs and descriptions
120-
toolsetIDs := tsg.ToolsetIDs()
121-
descriptions := tsg.ToolsetDescriptions()
122-
123-
// Filter out context and dynamic toolsets (handled separately)
124-
for _, id := range toolsetIDs {
125-
if id != "context" && id != "dynamic" {
126-
description := descriptions[id]
127-
lines = append(lines, fmt.Sprintf("| `%s` | %s |", id, description))
113+
buf.WriteString("| Toolset | Description |\n")
114+
buf.WriteString("| ----------------------- | ------------------------------------------------------------- |\n")
115+
116+
// Add the context toolset row with custom description (strongly recommended)
117+
buf.WriteString("| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n")
118+
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)
128131
}
129132
}
130133

131-
return strings.Join(lines, "\n")
134+
return strings.TrimSuffix(buf.String(), "\n")
132135
}
133136

134137
func generateToolsDoc(tsg *toolsets.ToolsetGroup) string {
135-
var sections []string
136-
137-
// Get toolset IDs (already sorted deterministically)
138-
toolsetIDs := tsg.ToolsetIDs()
138+
// AllTools() returns tools sorted by toolset ID then tool name.
139+
// We iterate once, grouping by toolset as we encounter them.
140+
tools := tsg.AllTools()
141+
if len(tools) == 0 {
142+
return ""
143+
}
139144

140-
for _, toolsetID := range toolsetIDs {
141-
if toolsetID == "dynamic" { // Skip dynamic toolset as it's handled separately
142-
continue
143-
}
145+
var buf strings.Builder
146+
var toolBuf strings.Builder
147+
var currentToolsetID toolsets.ToolsetID
148+
firstSection := true
144149

145-
// Get tools for this toolset (already sorted deterministically)
146-
tools := tsg.ToolsForToolset(toolsetID)
147-
if len(tools) == 0 {
148-
continue
150+
writeSection := func() {
151+
if toolBuf.Len() == 0 {
152+
return
149153
}
150-
151-
// Generate section header - capitalize first letter and replace underscores
152-
sectionName := formatToolsetName(string(toolsetID))
153-
154-
var toolDocs []string
155-
for _, serverTool := range tools {
156-
toolDoc := generateToolDoc(serverTool.Tool)
157-
toolDocs = append(toolDocs, toolDoc)
154+
if !firstSection {
155+
buf.WriteString("\n\n")
158156
}
157+
firstSection = false
158+
sectionName := formatToolsetName(string(currentToolsetID))
159+
fmt.Fprintf(&buf, "<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>", sectionName, strings.TrimSuffix(toolBuf.String(), "\n\n"))
160+
toolBuf.Reset()
161+
}
159162

160-
if len(toolDocs) > 0 {
161-
section := fmt.Sprintf("<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>",
162-
sectionName, strings.Join(toolDocs, "\n\n"))
163-
sections = append(sections, section)
163+
for _, tool := range tools {
164+
// When toolset changes, emit the previous section
165+
if tool.Toolset.ID != currentToolsetID {
166+
writeSection()
167+
currentToolsetID = tool.Toolset.ID
164168
}
169+
writeToolDoc(&toolBuf, tool.Tool)
170+
toolBuf.WriteString("\n\n")
165171
}
166172

167-
return strings.Join(sections, "\n\n")
173+
// Emit the last section
174+
writeSection()
175+
176+
return buf.String()
168177
}
169178

170179
func formatToolsetName(name string) string {
@@ -191,21 +200,19 @@ func formatToolsetName(name string) string {
191200
}
192201
}
193202

194-
func generateToolDoc(tool mcp.Tool) string {
195-
var lines []string
196-
203+
func writeToolDoc(buf *strings.Builder, tool mcp.Tool) {
197204
// Tool name only (using annotation name instead of verbose description)
198-
lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title))
205+
fmt.Fprintf(buf, "- **%s** - %s\n", tool.Name, tool.Annotations.Title)
199206

200207
// Parameters
201208
if tool.InputSchema == nil {
202-
lines = append(lines, " - No parameters required")
203-
return strings.Join(lines, "\n")
209+
buf.WriteString(" - No parameters required")
210+
return
204211
}
205212
schema, ok := tool.InputSchema.(*jsonschema.Schema)
206213
if !ok || schema == nil {
207-
lines = append(lines, " - No parameters required")
208-
return strings.Join(lines, "\n")
214+
buf.WriteString(" - No parameters required")
215+
return
209216
}
210217

211218
if len(schema.Properties) > 0 {
@@ -216,15 +223,15 @@ func generateToolDoc(tool mcp.Tool) string {
216223
}
217224
sort.Strings(paramNames)
218225

219-
for _, propName := range paramNames {
226+
for i, propName := range paramNames {
220227
prop := schema.Properties[propName]
221228
required := contains(schema.Required, propName)
222229
requiredStr := "optional"
223230
if required {
224231
requiredStr = "required"
225232
}
226233

227-
var typeStr, description string
234+
var typeStr string
228235

229236
// Get the type and description
230237
switch prop.Type {
@@ -238,19 +245,17 @@ func generateToolDoc(tool mcp.Tool) string {
238245
typeStr = prop.Type
239246
}
240247

241-
description = prop.Description
242-
243248
// Indent any continuation lines in the description to maintain markdown formatting
244-
description = indentMultilineDescription(description, " ")
249+
description := indentMultilineDescription(prop.Description, " ")
245250

246-
paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
247-
lines = append(lines, paramLine)
251+
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
252+
if i < len(paramNames)-1 {
253+
buf.WriteString("\n")
254+
}
248255
}
249256
} else {
250-
lines = append(lines, " - No parameters required")
257+
buf.WriteString(" - No parameters required")
251258
}
252-
253-
return strings.Join(lines, "\n")
254259
}
255260

256261
func contains(slice []string, item string) bool {
@@ -265,14 +270,18 @@ func contains(slice []string, item string) bool {
265270
// indentMultilineDescription adds the specified indent to all lines after the first line.
266271
// This ensures that multi-line descriptions maintain proper markdown list formatting.
267272
func indentMultilineDescription(description, indent string) string {
268-
lines := strings.Split(description, "\n")
269-
if len(lines) <= 1 {
273+
if !strings.Contains(description, "\n") {
270274
return description
271275
}
276+
var buf strings.Builder
277+
lines := strings.Split(description, "\n")
278+
buf.WriteString(lines[0])
272279
for i := 1; i < len(lines); i++ {
273-
lines[i] = indent + lines[i]
280+
buf.WriteString("\n")
281+
buf.WriteString(indent)
282+
buf.WriteString(lines[i])
274283
}
275-
return strings.Join(lines, "\n")
284+
return buf.String()
276285
}
277286

278287
func replaceSection(content, startMarker, endMarker, newContent string) string {
@@ -299,47 +308,49 @@ func generateRemoteToolsetsDoc() string {
299308
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
300309
buf.WriteString("|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n")
301310

302-
// Get toolset IDs and descriptions
303-
toolsetIDs := tsg.ToolsetIDs()
304-
descriptions := tsg.ToolsetDescriptions()
305-
306311
// Add "all" toolset first (special case)
307312
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")
308313

309-
// Add individual toolsets
310-
for _, id := range toolsetIDs {
311-
idStr := string(id)
312-
if idStr == "context" || idStr == "dynamic" { // Skip context and dynamic toolsets as they're handled separately
313-
continue
314-
}
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+
}
315326

316-
description := descriptions[id]
317-
formattedName := formatToolsetName(idStr)
318-
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
319-
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
320-
321-
// Create install config JSON (URL encoded)
322-
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
323-
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
324-
325-
// Fix URL encoding to use %20 instead of + for spaces
326-
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
327-
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
328-
329-
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
330-
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
331-
332-
buf.WriteString(fmt.Sprintf("| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
333-
formattedName,
334-
description,
335-
apiURL,
336-
installLink,
337-
fmt.Sprintf("[read-only](%s)", readonlyURL),
338-
readonlyInstallLink,
339-
))
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+
}
340351
}
341352

342-
return buf.String()
353+
return strings.TrimSuffix(buf.String(), "\n")
343354
}
344355

345356
func generateDeprecatedAliasesDocs(docsPath string) error {
@@ -366,15 +377,15 @@ func generateDeprecatedAliasesDocs(docsPath string) error {
366377
}
367378

368379
func generateDeprecatedAliasesTable() string {
369-
var lines []string
380+
var buf strings.Builder
370381

371382
// Add table header
372-
lines = append(lines, "| Old Name | New Name |")
373-
lines = append(lines, "|----------|----------|")
383+
buf.WriteString("| Old Name | New Name |\n")
384+
buf.WriteString("|----------|----------|\n")
374385

375386
aliases := github.DeprecatedToolAliases
376387
if len(aliases) == 0 {
377-
lines = append(lines, "| *(none currently)* | |")
388+
buf.WriteString("| *(none currently)* | |")
378389
} else {
379390
// Sort keys for deterministic output
380391
var oldNames []string
@@ -383,11 +394,14 @@ func generateDeprecatedAliasesTable() string {
383394
}
384395
sort.Strings(oldNames)
385396

386-
for _, oldName := range oldNames {
397+
for i, oldName := range oldNames {
387398
newName := aliases[oldName]
388-
lines = append(lines, fmt.Sprintf("| `%s` | `%s` |", oldName, newName))
399+
fmt.Fprintf(&buf, "| `%s` | `%s` |", oldName, newName)
400+
if i < len(oldNames)-1 {
401+
buf.WriteString("\n")
402+
}
389403
}
390404
}
391405

392-
return strings.Join(lines, "\n")
406+
return buf.String()
393407
}

docs/remote-server.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
3737
| Security Advisories | Security advisories related tools | https://api.githubcopilot.com/mcp/x/security_advisories | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/security_advisories/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%2Freadonly%22%7D) |
3838
| Stargazers | GitHub Stargazers related tools | https://api.githubcopilot.com/mcp/x/stargazers | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-stargazers&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fstargazers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/stargazers/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-stargazers&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fstargazers%2Freadonly%22%7D) |
3939
| Users | GitHub User related tools | https://api.githubcopilot.com/mcp/x/users | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/users/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%2Freadonly%22%7D) |
40-
4140
<!-- END AUTOMATED TOOLSETS -->
4241

4342
### Additional _Remote_ Server Toolsets

0 commit comments

Comments
 (0)