Skip to content

Commit dcdc124

Browse files
update documentation, add script and workflow
1 parent 798e674 commit dcdc124

File tree

11 files changed

+1109
-381
lines changed

11 files changed

+1109
-381
lines changed

.github/workflows/docs-check.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Documentation Check
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
docs-check:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version-file: 'go.mod'
23+
24+
- name: Build docs generator
25+
run: go build -o github-mcp-server ./cmd/github-mcp-server
26+
27+
- name: Generate documentation
28+
run: ./github-mcp-server generate-docs --readme-path README.md
29+
30+
- name: Check for documentation changes
31+
run: |
32+
if ! git diff --exit-code README.md; then
33+
echo "❌ Documentation is out of date!"
34+
echo ""
35+
echo "The generated documentation differs from what's committed."
36+
echo "Please run the following command to update the documentation:"
37+
echo ""
38+
echo " go run ./cmd/github-mcp-server generate-docs"
39+
echo ""
40+
echo "Then commit the changes."
41+
echo ""
42+
echo "Changes detected:"
43+
git diff README.md
44+
exit 1
45+
else
46+
echo "✅ Documentation is up to date!"
47+
fi

README.md

Lines changed: 303 additions & 373 deletions
Large diffs are not rendered by default.
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"os"
8+
"regexp"
9+
"sort"
10+
"strings"
11+
12+
"github.com/github/github-mcp-server/pkg/github"
13+
"github.com/github/github-mcp-server/pkg/raw"
14+
"github.com/github/github-mcp-server/pkg/toolsets"
15+
"github.com/github/github-mcp-server/pkg/translations"
16+
gogithub "github.com/google/go-github/v72/github"
17+
"github.com/mark3labs/mcp-go/mcp"
18+
"github.com/shurcooL/githubv4"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
var generateDocsCmd = &cobra.Command{
23+
Use: "generate-docs",
24+
Short: "Generate documentation for tools and toolsets",
25+
Long: `Generate the automated sections of README.md and docs/remote-server.md with current tool and toolset information.`,
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
return generateAllDocs()
28+
},
29+
}
30+
31+
func init() {
32+
rootCmd.AddCommand(generateDocsCmd)
33+
}
34+
35+
// mockGetClient returns a mock GitHub client for documentation generation
36+
func mockGetClient(ctx context.Context) (*gogithub.Client, error) {
37+
return gogithub.NewClient(nil), nil
38+
}
39+
40+
// mockGetGQLClient returns a mock GraphQL client for documentation generation
41+
func mockGetGQLClient(ctx context.Context) (*githubv4.Client, error) {
42+
return githubv4.NewClient(nil), nil
43+
}
44+
45+
// mockGetRawClient returns a mock raw client for documentation generation
46+
func mockGetRawClient(ctx context.Context) (*raw.Client, error) {
47+
return nil, nil
48+
}
49+
50+
func generateAllDocs() error {
51+
if err := generateReadmeDocs("README.md"); err != nil {
52+
return fmt.Errorf("failed to generate README docs: %w", err)
53+
}
54+
55+
if err := generateRemoteServerDocs("docs/remote-server.md"); err != nil {
56+
return fmt.Errorf("failed to generate remote-server docs: %w", err)
57+
}
58+
59+
return nil
60+
}
61+
62+
func generateReadmeDocs(readmePath string) error {
63+
// Create translation helper
64+
t, _ := translations.TranslationHelper()
65+
66+
// Create toolset group with mock clients
67+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t)
68+
69+
// Generate toolsets documentation
70+
toolsetsDoc := generateToolsetsDoc(tsg)
71+
72+
// Generate tools documentation
73+
toolsDoc := generateToolsDoc(tsg)
74+
75+
// Read the current README.md
76+
// #nosec G304 - readmePath is controlled by command line flag, not user input
77+
content, err := os.ReadFile(readmePath)
78+
if err != nil {
79+
return fmt.Errorf("failed to read README.md: %w", err)
80+
}
81+
82+
// Replace toolsets section
83+
updatedContent := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
84+
85+
// Replace tools section
86+
updatedContent = replaceSection(updatedContent, "START AUTOMATED TOOLS", "END AUTOMATED TOOLS", toolsDoc)
87+
88+
// Write back to file
89+
err = os.WriteFile(readmePath, []byte(updatedContent), 0600)
90+
if err != nil {
91+
return fmt.Errorf("failed to write README.md: %w", err)
92+
}
93+
94+
fmt.Println("Successfully updated README.md with automated documentation")
95+
return nil
96+
}
97+
98+
func generateRemoteServerDocs(docsPath string) error {
99+
content, err := os.ReadFile(docsPath) //#nosec G304
100+
if err != nil {
101+
return fmt.Errorf("failed to read docs file: %w", err)
102+
}
103+
104+
toolsetsDoc := generateRemoteToolsetsDoc()
105+
106+
// Replace content between markers
107+
startMarker := "<!-- START AUTOMATED TOOLSETS -->"
108+
endMarker := "<!-- END AUTOMATED TOOLSETS -->"
109+
110+
contentStr := string(content)
111+
startIndex := strings.Index(contentStr, startMarker)
112+
endIndex := strings.Index(contentStr, endMarker)
113+
114+
if startIndex == -1 || endIndex == -1 {
115+
return fmt.Errorf("automation markers not found in %s", docsPath)
116+
}
117+
118+
newContent := contentStr[:startIndex] + startMarker + "\n" + toolsetsDoc + "\n" + endMarker + contentStr[endIndex+len(endMarker):]
119+
120+
return os.WriteFile(docsPath, []byte(newContent), 0600) //#nosec G306
121+
}
122+
123+
func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
124+
var lines []string
125+
126+
// Add table header and separator
127+
lines = append(lines, "| Toolset | Description |")
128+
lines = append(lines, "| ----------------------- | ------------------------------------------------------------- |")
129+
130+
// Add the context toolset row (handled separately in README)
131+
lines = append(lines, "| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |")
132+
133+
// Get all toolsets except context (which is handled separately above)
134+
var toolsetNames []string
135+
for name := range tsg.Toolsets {
136+
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
137+
toolsetNames = append(toolsetNames, name)
138+
}
139+
}
140+
141+
// Sort toolset names for consistent output
142+
sort.Strings(toolsetNames)
143+
144+
for _, name := range toolsetNames {
145+
toolset := tsg.Toolsets[name]
146+
lines = append(lines, fmt.Sprintf("| `%s` | %s |", name, toolset.Description))
147+
}
148+
149+
return strings.Join(lines, "\n")
150+
}
151+
152+
func generateToolsDoc(tsg *toolsets.ToolsetGroup) string {
153+
var sections []string
154+
155+
// Define the order of toolsets to match the existing README
156+
orderedToolsets := []string{"context", "issues", "pull_requests", "repos", "actions", "code_security", "secret_protection", "notifications", "users", "orgs", "experiments"}
157+
158+
for _, toolsetName := range orderedToolsets {
159+
toolset, exists := tsg.Toolsets[toolsetName]
160+
if !exists {
161+
continue
162+
}
163+
164+
tools := toolset.GetAvailableTools()
165+
if len(tools) == 0 {
166+
continue
167+
}
168+
169+
// Sort tools by name for deterministic order
170+
sort.Slice(tools, func(i, j int) bool {
171+
return tools[i].Tool.Name < tools[j].Tool.Name
172+
})
173+
174+
// Generate section header - capitalize first letter and replace underscores
175+
sectionName := formatToolsetName(toolsetName)
176+
177+
var toolDocs []string
178+
for _, serverTool := range tools {
179+
toolDoc := generateToolDoc(serverTool.Tool)
180+
toolDocs = append(toolDocs, toolDoc)
181+
}
182+
183+
if len(toolDocs) > 0 {
184+
section := fmt.Sprintf("<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>",
185+
sectionName, strings.Join(toolDocs, "\n\n"))
186+
sections = append(sections, section)
187+
}
188+
}
189+
190+
return strings.Join(sections, "\n\n")
191+
}
192+
193+
func formatToolsetName(name string) string {
194+
switch name {
195+
case "pull_requests":
196+
return "Pull Requests"
197+
case "repos":
198+
return "Repositories"
199+
case "code_security":
200+
return "Code Security"
201+
case "secret_protection":
202+
return "Secret Protection"
203+
case "orgs":
204+
return "Organizations"
205+
default:
206+
// Fallback: capitalize first letter and replace underscores with spaces
207+
parts := strings.Split(name, "_")
208+
for i, part := range parts {
209+
if len(part) > 0 {
210+
parts[i] = strings.ToUpper(string(part[0])) + part[1:]
211+
}
212+
}
213+
return strings.Join(parts, " ")
214+
}
215+
}
216+
217+
func generateToolDoc(tool mcp.Tool) string {
218+
var lines []string
219+
220+
// Tool name only (using annotation name instead of verbose description)
221+
lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title))
222+
223+
// Parameters
224+
schema := tool.InputSchema
225+
if len(schema.Properties) > 0 {
226+
// Get parameter names and sort them for deterministic order
227+
var paramNames []string
228+
for propName := range schema.Properties {
229+
paramNames = append(paramNames, propName)
230+
}
231+
sort.Strings(paramNames)
232+
233+
for _, propName := range paramNames {
234+
prop := schema.Properties[propName]
235+
required := contains(schema.Required, propName)
236+
requiredStr := "optional"
237+
if required {
238+
requiredStr = "required"
239+
}
240+
241+
// Get the type and description
242+
typeStr := "unknown"
243+
description := ""
244+
245+
if propMap, ok := prop.(map[string]interface{}); ok {
246+
if typeVal, ok := propMap["type"].(string); ok {
247+
if typeVal == "array" {
248+
if items, ok := propMap["items"].(map[string]interface{}); ok {
249+
if itemType, ok := items["type"].(string); ok {
250+
typeStr = itemType + "[]"
251+
}
252+
} else {
253+
typeStr = "array"
254+
}
255+
} else {
256+
typeStr = typeVal
257+
}
258+
}
259+
260+
if desc, ok := propMap["description"].(string); ok {
261+
description = desc
262+
}
263+
}
264+
265+
paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
266+
lines = append(lines, paramLine)
267+
}
268+
} else {
269+
lines = append(lines, " - No parameters required")
270+
}
271+
272+
return strings.Join(lines, "\n")
273+
}
274+
275+
func contains(slice []string, item string) bool {
276+
for _, s := range slice {
277+
if s == item {
278+
return true
279+
}
280+
}
281+
return false
282+
}
283+
284+
func replaceSection(content, startMarker, endMarker, newContent string) string {
285+
startPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(startMarker))
286+
endPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(endMarker))
287+
288+
re := regexp.MustCompile(fmt.Sprintf(`(?s)%s.*?%s`, startPattern, endPattern))
289+
290+
replacement := fmt.Sprintf("<!-- %s -->\n%s\n<!-- %s -->", startMarker, newContent, endMarker)
291+
292+
return re.ReplaceAllString(content, replacement)
293+
}
294+
295+
func generateRemoteToolsetsDoc() string {
296+
var buf strings.Builder
297+
298+
// Create translation helper
299+
t, _ := translations.TranslationHelper()
300+
301+
// Create toolset group with mock clients
302+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t)
303+
304+
// Generate table header
305+
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
306+
buf.WriteString("|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n")
307+
308+
// Get all toolsets
309+
toolsetNames := make([]string, 0, len(tsg.Toolsets))
310+
for name := range tsg.Toolsets {
311+
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
312+
toolsetNames = append(toolsetNames, name)
313+
}
314+
}
315+
sort.Strings(toolsetNames)
316+
317+
// Add "all" toolset first (special case)
318+
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")
319+
320+
// Add individual toolsets
321+
for _, name := range toolsetNames {
322+
toolset := tsg.Toolsets[name]
323+
324+
formattedName := formatToolsetName(name)
325+
description := toolset.Description
326+
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", name)
327+
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", name)
328+
329+
// Create install config JSON (URL encoded)
330+
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
331+
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
332+
333+
// Fix URL encoding to use %20 instead of + for spaces
334+
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
335+
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
336+
337+
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, installConfig)
338+
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, readonlyConfig)
339+
340+
buf.WriteString(fmt.Sprintf("| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
341+
formattedName,
342+
description,
343+
apiURL,
344+
installLink,
345+
fmt.Sprintf("[read-only](%s)", readonlyURL),
346+
readonlyInstallLink,
347+
))
348+
}
349+
350+
return buf.String()
351+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy