adowu commited on
Commit
99e68f8
·
verified ·
1 Parent(s): 752268e

Upload 2 files

Browse files
Files changed (2) hide show
  1. generate_docs.go +354 -0
  2. main.go +116 -0
generate_docs.go ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/v74/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(_ *cobra.Command, _ []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(_ 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(_ 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(_ 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, 5000)
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
+ // Get all toolset names and sort them alphabetically for deterministic order
156
+ var toolsetNames []string
157
+ for name := range tsg.Toolsets {
158
+ if name != "dynamic" { // Skip dynamic toolset as it's handled separately
159
+ toolsetNames = append(toolsetNames, name)
160
+ }
161
+ }
162
+ sort.Strings(toolsetNames)
163
+
164
+ for _, toolsetName := range toolsetNames {
165
+ toolset := tsg.Toolsets[toolsetName]
166
+
167
+ tools := toolset.GetAvailableTools()
168
+ if len(tools) == 0 {
169
+ continue
170
+ }
171
+
172
+ // Sort tools by name for deterministic order
173
+ sort.Slice(tools, func(i, j int) bool {
174
+ return tools[i].Tool.Name < tools[j].Tool.Name
175
+ })
176
+
177
+ // Generate section header - capitalize first letter and replace underscores
178
+ sectionName := formatToolsetName(toolsetName)
179
+
180
+ var toolDocs []string
181
+ for _, serverTool := range tools {
182
+ toolDoc := generateToolDoc(serverTool.Tool)
183
+ toolDocs = append(toolDocs, toolDoc)
184
+ }
185
+
186
+ if len(toolDocs) > 0 {
187
+ section := fmt.Sprintf("<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>",
188
+ sectionName, strings.Join(toolDocs, "\n\n"))
189
+ sections = append(sections, section)
190
+ }
191
+ }
192
+
193
+ return strings.Join(sections, "\n\n")
194
+ }
195
+
196
+ func formatToolsetName(name string) string {
197
+ switch name {
198
+ case "pull_requests":
199
+ return "Pull Requests"
200
+ case "repos":
201
+ return "Repositories"
202
+ case "code_security":
203
+ return "Code Security"
204
+ case "secret_protection":
205
+ return "Secret Protection"
206
+ case "orgs":
207
+ return "Organizations"
208
+ default:
209
+ // Fallback: capitalize first letter and replace underscores with spaces
210
+ parts := strings.Split(name, "_")
211
+ for i, part := range parts {
212
+ if len(part) > 0 {
213
+ parts[i] = strings.ToUpper(string(part[0])) + part[1:]
214
+ }
215
+ }
216
+ return strings.Join(parts, " ")
217
+ }
218
+ }
219
+
220
+ func generateToolDoc(tool mcp.Tool) string {
221
+ var lines []string
222
+
223
+ // Tool name only (using annotation name instead of verbose description)
224
+ lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title))
225
+
226
+ // Parameters
227
+ schema := tool.InputSchema
228
+ if len(schema.Properties) > 0 {
229
+ // Get parameter names and sort them for deterministic order
230
+ var paramNames []string
231
+ for propName := range schema.Properties {
232
+ paramNames = append(paramNames, propName)
233
+ }
234
+ sort.Strings(paramNames)
235
+
236
+ for _, propName := range paramNames {
237
+ prop := schema.Properties[propName]
238
+ required := contains(schema.Required, propName)
239
+ requiredStr := "optional"
240
+ if required {
241
+ requiredStr = "required"
242
+ }
243
+
244
+ // Get the type and description
245
+ typeStr := "unknown"
246
+ description := ""
247
+
248
+ if propMap, ok := prop.(map[string]interface{}); ok {
249
+ if typeVal, ok := propMap["type"].(string); ok {
250
+ if typeVal == "array" {
251
+ if items, ok := propMap["items"].(map[string]interface{}); ok {
252
+ if itemType, ok := items["type"].(string); ok {
253
+ typeStr = itemType + "[]"
254
+ }
255
+ } else {
256
+ typeStr = "array"
257
+ }
258
+ } else {
259
+ typeStr = typeVal
260
+ }
261
+ }
262
+
263
+ if desc, ok := propMap["description"].(string); ok {
264
+ description = desc
265
+ }
266
+ }
267
+
268
+ paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
269
+ lines = append(lines, paramLine)
270
+ }
271
+ } else {
272
+ lines = append(lines, " - No parameters required")
273
+ }
274
+
275
+ return strings.Join(lines, "\n")
276
+ }
277
+
278
+ func contains(slice []string, item string) bool {
279
+ for _, s := range slice {
280
+ if s == item {
281
+ return true
282
+ }
283
+ }
284
+ return false
285
+ }
286
+
287
+ func replaceSection(content, startMarker, endMarker, newContent string) string {
288
+ startPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(startMarker))
289
+ endPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(endMarker))
290
+
291
+ re := regexp.MustCompile(fmt.Sprintf(`(?s)%s.*?%s`, startPattern, endPattern))
292
+
293
+ replacement := fmt.Sprintf("<!-- %s -->\n%s\n<!-- %s -->", startMarker, newContent, endMarker)
294
+
295
+ return re.ReplaceAllString(content, replacement)
296
+ }
297
+
298
+ func generateRemoteToolsetsDoc() string {
299
+ var buf strings.Builder
300
+
301
+ // Create translation helper
302
+ t, _ := translations.TranslationHelper()
303
+
304
+ // Create toolset group with mock clients
305
+ tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
306
+
307
+ // Generate table header
308
+ buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
309
+ buf.WriteString("|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n")
310
+
311
+ // Get all toolsets
312
+ toolsetNames := make([]string, 0, len(tsg.Toolsets))
313
+ for name := range tsg.Toolsets {
314
+ if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
315
+ toolsetNames = append(toolsetNames, name)
316
+ }
317
+ }
318
+ sort.Strings(toolsetNames)
319
+
320
+ // Add "all" toolset first (special case)
321
+ 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")
322
+
323
+ // Add individual toolsets
324
+ for _, name := range toolsetNames {
325
+ toolset := tsg.Toolsets[name]
326
+
327
+ formattedName := formatToolsetName(name)
328
+ description := toolset.Description
329
+ apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", name)
330
+ readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", name)
331
+
332
+ // Create install config JSON (URL encoded)
333
+ installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
334
+ readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
335
+
336
+ // Fix URL encoding to use %20 instead of + for spaces
337
+ installConfig = strings.ReplaceAll(installConfig, "+", "%20")
338
+ readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
339
+
340
+ installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, installConfig)
341
+ readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, readonlyConfig)
342
+
343
+ buf.WriteString(fmt.Sprintf("| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
344
+ formattedName,
345
+ description,
346
+ apiURL,
347
+ installLink,
348
+ fmt.Sprintf("[read-only](%s)", readonlyURL),
349
+ readonlyInstallLink,
350
+ ))
351
+ }
352
+
353
+ return buf.String()
354
+ }
main.go ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/github/github-mcp-server/internal/ghmcp"
10
+ "github.com/github/github-mcp-server/pkg/github"
11
+ "github.com/spf13/cobra"
12
+ "github.com/spf13/pflag"
13
+ "github.com/spf13/viper"
14
+ )
15
+
16
+ // These variables are set by the build process using ldflags.
17
+ var version = "version"
18
+ var commit = "commit"
19
+ var date = "date"
20
+
21
+ var (
22
+ rootCmd = &cobra.Command{
23
+ Use: "server",
24
+ Short: "GitHub MCP Server",
25
+ Long: `A GitHub MCP server that handles various tools and resources.`,
26
+ Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date),
27
+ }
28
+
29
+ stdioCmd = &cobra.Command{
30
+ Use: "stdio",
31
+ Short: "Start stdio server",
32
+ Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
33
+ RunE: func(_ *cobra.Command, _ []string) error {
34
+ token := viper.GetString("personal_access_token")
35
+ if token == "" {
36
+ return errors.New("GITHUB_PERSONAL_ACCESS_TOKEN not set")
37
+ }
38
+
39
+ // If you're wondering why we're not using viper.GetStringSlice("toolsets"),
40
+ // it's because viper doesn't handle comma-separated values correctly for env
41
+ // vars when using GetStringSlice.
42
+ // https://github.com/spf13/viper/issues/380
43
+ var enabledToolsets []string
44
+ if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
45
+ return fmt.Errorf("failed to unmarshal toolsets: %w", err)
46
+ }
47
+
48
+ stdioServerConfig := ghmcp.StdioServerConfig{
49
+ Version: version,
50
+ Host: viper.GetString("host"),
51
+ Token: token,
52
+ EnabledToolsets: enabledToolsets,
53
+ DynamicToolsets: viper.GetBool("dynamic_toolsets"),
54
+ ReadOnly: viper.GetBool("read-only"),
55
+ ExportTranslations: viper.GetBool("export-translations"),
56
+ EnableCommandLogging: viper.GetBool("enable-command-logging"),
57
+ LogFilePath: viper.GetString("log-file"),
58
+ ContentWindowSize: viper.GetInt("content-window-size"),
59
+ }
60
+ return ghmcp.RunStdioServer(stdioServerConfig)
61
+ },
62
+ }
63
+ )
64
+
65
+ func init() {
66
+ cobra.OnInitialize(initConfig)
67
+ rootCmd.SetGlobalNormalizationFunc(wordSepNormalizeFunc)
68
+
69
+ rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n")
70
+
71
+ // Add global flags that will be shared by all commands
72
+ rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all")
73
+ rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
74
+ rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
75
+ rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
76
+ rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
77
+ rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
78
+ rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
79
+ rootCmd.PersistentFlags().Int("content-window-size", 5000, "Specify the content window size")
80
+
81
+ // Bind flag to viper
82
+ _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
83
+ _ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
84
+ _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
85
+ _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
86
+ _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
87
+ _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
88
+ _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
89
+ _ = viper.BindPFlag("content-window-size", rootCmd.PersistentFlags().Lookup("content-window-size"))
90
+
91
+ // Add subcommands
92
+ rootCmd.AddCommand(stdioCmd)
93
+ }
94
+
95
+ func initConfig() {
96
+ // Initialize Viper configuration
97
+ viper.SetEnvPrefix("github")
98
+ viper.AutomaticEnv()
99
+
100
+ }
101
+
102
+ func main() {
103
+ if err := rootCmd.Execute(); err != nil {
104
+ fmt.Fprintf(os.Stderr, "%v\n", err)
105
+ os.Exit(1)
106
+ }
107
+ }
108
+
109
+ func wordSepNormalizeFunc(_ *pflag.FlagSet, name string) pflag.NormalizedName {
110
+ from := []string{"_"}
111
+ to := "-"
112
+ for _, sep := range from {
113
+ name = strings.ReplaceAll(name, sep, to)
114
+ }
115
+ return pflag.NormalizedName(name)
116
+ }