Skip to content

Commit 3929d21

Browse files
docs: auto-generate per-flag tool lists for insiders and feature flags
Adds two auto-generated documentation sections that describe how feature flags shape the tool surface: - docs/insiders-features.md gets a per-flag block under its existing hand-written prose. Each Insiders flag whose tools differ from the default surface is listed with the full tool schema rendered through the same writer used for README, so contributors can see exactly what Insiders Mode adds or changes. - docs/feature-flags.md is new and gives the same treatment to every flag in AllowedFeatureFlags (user-controllable flags). It links back to the Insiders doc for the auto-enabled subset. Both sections are produced by a single generator that diffs the flag-on inventory against the default-flagged inventory and reports any tool that is new or has a different InputSchema/Meta. No reason classification - just tools and their schemas, kept intentionally simple so contributors don't have to update the generator when adding a new flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6fd9d07 commit 3929d21

4 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"reflect"
8+
"sort"
9+
"strings"
10+
11+
"github.com/github/github-mcp-server/pkg/github"
12+
"github.com/github/github-mcp-server/pkg/inventory"
13+
"github.com/github/github-mcp-server/pkg/translations"
14+
)
15+
16+
// generateInsidersFeaturesDocs refreshes the auto-generated section of
17+
// docs/insiders-features.md with the tools and schemas affected by each
18+
// Insiders feature flag.
19+
func generateInsidersFeaturesDocs(docsPath string) error {
20+
body := generateFlaggedToolsDoc(github.InsidersFeatureFlags, "_No Insiders-only tool changes._")
21+
return rewriteAutomatedSection(docsPath, "START AUTOMATED INSIDERS TOOLS", "END AUTOMATED INSIDERS TOOLS", body)
22+
}
23+
24+
// generateFeatureFlagsDocs refreshes the auto-generated section of
25+
// docs/feature-flags.md with the tools and schemas affected by each
26+
// user-controllable feature flag.
27+
func generateFeatureFlagsDocs(docsPath string) error {
28+
body := generateFlaggedToolsDoc(github.AllowedFeatureFlags, "_No user-controllable feature flags affect tool registration._")
29+
return rewriteAutomatedSection(docsPath, "START AUTOMATED FEATURE FLAG TOOLS", "END AUTOMATED FEATURE FLAG TOOLS", body)
30+
}
31+
32+
// generateFlaggedToolsDoc renders, for each flag in the input set, the tools
33+
// whose registration or definition differs from the default user experience.
34+
// Each affected tool is printed with its full schema using the same writer
35+
// used by the README so the output style stays consistent.
36+
func generateFlaggedToolsDoc(flags []string, emptyMessage string) string {
37+
t, _ := translations.TranslationHelper()
38+
defaultTools := indexToolsByName(buildInventoryWithFlags(t, nil).AvailableTools(context.Background()))
39+
40+
var buf strings.Builder
41+
hasAny := false
42+
43+
for _, flag := range flags {
44+
affected := flaggedToolDiff(t, flag, defaultTools)
45+
if len(affected) == 0 {
46+
continue
47+
}
48+
49+
if hasAny {
50+
buf.WriteString("\n\n")
51+
}
52+
hasAny = true
53+
54+
fmt.Fprintf(&buf, "### `%s`\n\n", flag)
55+
for i, tool := range affected {
56+
writeToolDoc(&buf, tool)
57+
if i < len(affected)-1 {
58+
buf.WriteString("\n\n")
59+
}
60+
}
61+
}
62+
63+
if !hasAny {
64+
return emptyMessage
65+
}
66+
return strings.TrimSuffix(buf.String(), "\n")
67+
}
68+
69+
// flaggedToolDiff returns the tools whose definition (input schema or meta)
70+
// differs from the default-flagged inventory when only the given flag is on,
71+
// plus tools that exist only in the flag-on inventory. Results are sorted by
72+
// tool name.
73+
func flaggedToolDiff(t translations.TranslationHelperFunc, flag string, defaultTools map[string]inventory.ServerTool) []inventory.ServerTool {
74+
flagTools := buildInventoryWithFlags(t, map[string]bool{flag: true}).AvailableTools(context.Background())
75+
76+
out := make([]inventory.ServerTool, 0)
77+
seen := make(map[string]struct{}, len(flagTools))
78+
79+
for _, tool := range flagTools {
80+
if _, ok := seen[tool.Tool.Name]; ok {
81+
continue
82+
}
83+
seen[tool.Tool.Name] = struct{}{}
84+
85+
baseline, hadBaseline := defaultTools[tool.Tool.Name]
86+
if hadBaseline && reflect.DeepEqual(tool.Tool.InputSchema, baseline.Tool.InputSchema) && reflect.DeepEqual(tool.Tool.Meta, baseline.Tool.Meta) {
87+
continue
88+
}
89+
out = append(out, tool)
90+
}
91+
92+
sort.Slice(out, func(i, j int) bool { return out[i].Tool.Name < out[j].Tool.Name })
93+
return out
94+
}
95+
96+
// buildInventoryWithFlags constructs an inventory whose feature checker treats
97+
// the given flags as enabled and every other flag as disabled. Passing nil
98+
// produces the default-flagged inventory.
99+
func buildInventoryWithFlags(t translations.TranslationHelperFunc, enabled map[string]bool) *inventory.Inventory {
100+
checker := func(_ context.Context, flag string) (bool, error) {
101+
return enabled[flag], nil
102+
}
103+
inv, _ := github.NewInventory(t).
104+
WithToolsets([]string{"all"}).
105+
WithFeatureChecker(checker).
106+
Build()
107+
return inv
108+
}
109+
110+
// indexToolsByName returns a map keyed by tool name. When duplicates exist
111+
// (e.g. flag-gated dual registrations), the first occurrence wins, mirroring
112+
// AvailableTools' deterministic sort order.
113+
func indexToolsByName(tools []inventory.ServerTool) map[string]inventory.ServerTool {
114+
out := make(map[string]inventory.ServerTool, len(tools))
115+
for _, tool := range tools {
116+
if _, ok := out[tool.Tool.Name]; ok {
117+
continue
118+
}
119+
out[tool.Tool.Name] = tool
120+
}
121+
return out
122+
}
123+
124+
// rewriteAutomatedSection reads a markdown file, replaces the content between
125+
// the named markers with body, and writes it back.
126+
func rewriteAutomatedSection(path, startMarker, endMarker, body string) error {
127+
content, err := os.ReadFile(path) //#nosec G304
128+
if err != nil {
129+
return fmt.Errorf("failed to read docs file: %w", err)
130+
}
131+
updated, err := replaceSection(string(content), startMarker, endMarker, body)
132+
if err != nil {
133+
return err
134+
}
135+
return os.WriteFile(path, []byte(updated), 0600) //#nosec G306
136+
}

cmd/github-mcp-server/generate_docs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func generateAllDocs() error {
4343
// File to edit, function to generate its docs
4444
{"README.md", generateReadmeDocs},
4545
{"docs/remote-server.md", generateRemoteServerDocs},
46+
{"docs/insiders-features.md", generateInsidersFeaturesDocs},
47+
{"docs/feature-flags.md", generateFeatureFlagsDocs},
4648
{"docs/tool-renaming.md", generateDeprecatedAliasesDocs},
4749
} {
4850
if err := doc.fn(doc.path); err != nil {

docs/feature-flags.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Feature Flags
2+
3+
Feature flags let you opt into experimental tool behavior on top of the default
4+
GitHub MCP Server surface. Insiders Mode turns on a curated subset of these
5+
flags automatically — see [Insiders Features](./insiders-features.md) for that
6+
specific set.
7+
8+
For background on how flags resolve at request time, see the [resolution
9+
section in the Insiders docs](./insiders-features.md#how-feature-flags-are-resolved).
10+
11+
## Enabling a flag
12+
13+
| Method | Remote Server | Local Server |
14+
|--------|---------------|--------------|
15+
| Header | `X-MCP-Features: <flag>,<flag>` | N/A |
16+
| CLI flag | N/A | `--features=<flag>,<flag>` |
17+
| Environment variable | N/A | `GITHUB_FEATURES=<flag>,<flag>` |
18+
19+
Only flags listed in
20+
[`AllowedFeatureFlags`](../pkg/github/feature_flags.go) can be enabled by
21+
end users. Insiders-only flags are not user-toggleable.
22+
23+
---
24+
25+
## Tools affected by each flag
26+
27+
The table below is regenerated from the Go source. For each user-controllable
28+
feature flag, it lists every tool whose registration or input schema differs
29+
from the default — either because the flag introduces a new tool, or because
30+
it selects a flag-aware variant of an existing tool.
31+
32+
<!-- START AUTOMATED FEATURE FLAG TOOLS -->
33+
### `remote_mcp_issue_fields`
34+
35+
- **list_issue_fields** - List issue fields
36+
- **Required OAuth Scopes**: `repo`, `read:org`
37+
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
38+
- `owner`: The account owner of the repository or organization. The name is not case sensitive. (string, required)
39+
- `repo`: The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly. (string, optional)
40+
41+
- **list_issues** - List issues
42+
- **Required OAuth Scopes**: `repo`
43+
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
44+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
45+
- `field_filters`: Filter by custom issue field values. Each entry takes a field_name and a value; the server looks up the field and coerces the value to its type (single-select option name, text, number, or YYYY-MM-DD date). (object[], optional)
46+
- `labels`: Filter by labels (string[], optional)
47+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
48+
- `owner`: Repository owner (string, required)
49+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
50+
- `repo`: Repository name (string, required)
51+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
52+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
53+
54+
### `issues_granular`
55+
56+
- **add_sub_issue** - Add Sub-Issue
57+
- **Required OAuth Scopes**: `repo`
58+
- `issue_number`: The parent issue number (number, required)
59+
- `owner`: Repository owner (username or organization) (string, required)
60+
- `replace_parent`: If true, reparent the sub-issue if it already has a parent (boolean, optional)
61+
- `repo`: Repository name (string, required)
62+
- `sub_issue_id`: The ID of the sub-issue to add. ID is not the same as issue number (number, required)
63+
64+
- **create_issue** - Create Issue
65+
- **Required OAuth Scopes**: `repo`
66+
- `body`: Issue body content (optional) (string, optional)
67+
- `owner`: Repository owner (username or organization) (string, required)
68+
- `repo`: Repository name (string, required)
69+
- `title`: Issue title (string, required)
70+
71+
- **remove_sub_issue** - Remove Sub-Issue
72+
- **Required OAuth Scopes**: `repo`
73+
- `issue_number`: The parent issue number (number, required)
74+
- `owner`: Repository owner (username or organization) (string, required)
75+
- `repo`: Repository name (string, required)
76+
- `sub_issue_id`: The ID of the sub-issue to remove. ID is not the same as issue number (number, required)
77+
78+
- **reprioritize_sub_issue** - Reprioritize Sub-Issue
79+
- **Required OAuth Scopes**: `repo`
80+
- `after_id`: The ID of the sub-issue to place this after (either after_id OR before_id should be specified) (number, optional)
81+
- `before_id`: The ID of the sub-issue to place this before (either after_id OR before_id should be specified) (number, optional)
82+
- `issue_number`: The parent issue number (number, required)
83+
- `owner`: Repository owner (username or organization) (string, required)
84+
- `repo`: Repository name (string, required)
85+
- `sub_issue_id`: The ID of the sub-issue to reorder. ID is not the same as issue number (number, required)
86+
87+
- **set_issue_fields** - Set Issue Fields
88+
- **Required OAuth Scopes**: `repo`
89+
- `fields`: Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value. (object[], required)
90+
- `issue_number`: The issue number to update (number, required)
91+
- `owner`: Repository owner (username or organization) (string, required)
92+
- `repo`: Repository name (string, required)
93+
94+
- **update_issue_assignees** - Update Issue Assignees
95+
- **Required OAuth Scopes**: `repo`
96+
- `assignees`: GitHub usernames to assign to this issue (string[], required)
97+
- `issue_number`: The issue number to update (number, required)
98+
- `owner`: Repository owner (username or organization) (string, required)
99+
- `repo`: Repository name (string, required)
100+
101+
- **update_issue_body** - Update Issue Body
102+
- **Required OAuth Scopes**: `repo`
103+
- `body`: The new body content for the issue (string, required)
104+
- `issue_number`: The issue number to update (number, required)
105+
- `owner`: Repository owner (username or organization) (string, required)
106+
- `repo`: Repository name (string, required)
107+
108+
- **update_issue_labels** - Update Issue Labels
109+
- **Required OAuth Scopes**: `repo`
110+
- `issue_number`: The issue number to update (number, required)
111+
- `labels`: Labels to apply to this issue. ([], required)
112+
- `owner`: Repository owner (username or organization) (string, required)
113+
- `repo`: Repository name (string, required)
114+
115+
- **update_issue_milestone** - Update Issue Milestone
116+
- **Required OAuth Scopes**: `repo`
117+
- `issue_number`: The issue number to update (number, required)
118+
- `milestone`: The milestone number to set on the issue (integer, required)
119+
- `owner`: Repository owner (username or organization) (string, required)
120+
- `repo`: Repository name (string, required)
121+
122+
- **update_issue_state** - Update Issue State
123+
- **Required OAuth Scopes**: `repo`
124+
- `issue_number`: The issue number to update (number, required)
125+
- `owner`: Repository owner (username or organization) (string, required)
126+
- `repo`: Repository name (string, required)
127+
- `state`: The new state for the issue (string, required)
128+
- `state_reason`: The reason for the state change (only for closed state) (string, optional)
129+
130+
- **update_issue_title** - Update Issue Title
131+
- **Required OAuth Scopes**: `repo`
132+
- `issue_number`: The issue number to update (number, required)
133+
- `owner`: Repository owner (username or organization) (string, required)
134+
- `repo`: Repository name (string, required)
135+
- `title`: The new title for the issue (string, required)
136+
137+
- **update_issue_type** - Update Issue Type
138+
- **Required OAuth Scopes**: `repo`
139+
- `issue_number`: The issue number to update (number, required)
140+
- `issue_type`: The issue type to set (string, required)
141+
- `owner`: Repository owner (username or organization) (string, required)
142+
- `rationale`: One concise sentence explaining what specifically about the issue led you to choose this type. State the concrete signal (e.g. 'Reports a crash when saving' → bug, 'Asks for dark mode support' → feature). (string, optional)
143+
- `repo`: Repository name (string, required)
144+
145+
### `pull_requests_granular`
146+
147+
- **add_pull_request_review_comment** - Add Pull Request Review Comment
148+
- **Required OAuth Scopes**: `repo`
149+
- `body`: The comment body (string, required)
150+
- `line`: The line number in the diff to comment on (optional) (number, optional)
151+
- `owner`: Repository owner (username or organization) (string, required)
152+
- `path`: The relative path of the file to comment on (string, required)
153+
- `pullNumber`: The pull request number (number, required)
154+
- `repo`: Repository name (string, required)
155+
- `side`: The side of the diff to comment on (optional) (string, optional)
156+
- `startLine`: The start line of a multi-line comment (optional) (number, optional)
157+
- `startSide`: The start side of a multi-line comment (optional) (string, optional)
158+
- `subjectType`: The subject type of the comment (string, required)
159+
160+
- **create_pull_request_review** - Create Pull Request Review
161+
- **Required OAuth Scopes**: `repo`
162+
- `body`: The review body text (optional) (string, optional)
163+
- `commitID`: The SHA of the commit to review (optional, defaults to latest) (string, optional)
164+
- `event`: The review action to perform. If omitted, creates a pending review. (string, optional)
165+
- `owner`: Repository owner (username or organization) (string, required)
166+
- `pullNumber`: The pull request number (number, required)
167+
- `repo`: Repository name (string, required)
168+
169+
- **delete_pending_pull_request_review** - Delete Pending Pull Request Review
170+
- **Required OAuth Scopes**: `repo`
171+
- `owner`: Repository owner (username or organization) (string, required)
172+
- `pullNumber`: The pull request number (number, required)
173+
- `repo`: Repository name (string, required)
174+
175+
- **request_pull_request_reviewers** - Request Pull Request Reviewers
176+
- **Required OAuth Scopes**: `repo`
177+
- `owner`: Repository owner (username or organization) (string, required)
178+
- `pullNumber`: The pull request number (number, required)
179+
- `repo`: Repository name (string, required)
180+
- `reviewers`: GitHub usernames to request reviews from (string[], required)
181+
182+
- **resolve_review_thread** - Resolve Review Thread
183+
- **Required OAuth Scopes**: `repo`
184+
- `threadID`: The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx) (string, required)
185+
186+
- **submit_pending_pull_request_review** - Submit Pending Pull Request Review
187+
- **Required OAuth Scopes**: `repo`
188+
- `body`: The review body text (optional) (string, optional)
189+
- `event`: The review action to perform (string, required)
190+
- `owner`: Repository owner (username or organization) (string, required)
191+
- `pullNumber`: The pull request number (number, required)
192+
- `repo`: Repository name (string, required)
193+
194+
- **unresolve_review_thread** - Unresolve Review Thread
195+
- **Required OAuth Scopes**: `repo`
196+
- `threadID`: The node ID of the review thread to unresolve (e.g., PRRT_kwDOxxx) (string, required)
197+
198+
- **update_pull_request_body** - Update Pull Request Body
199+
- **Required OAuth Scopes**: `repo`
200+
- `body`: The new body content for the pull request (string, required)
201+
- `owner`: Repository owner (username or organization) (string, required)
202+
- `pullNumber`: The pull request number (number, required)
203+
- `repo`: Repository name (string, required)
204+
205+
- **update_pull_request_draft_state** - Update Pull Request Draft State
206+
- **Required OAuth Scopes**: `repo`
207+
- `draft`: Set to true to convert to draft, false to mark as ready for review (boolean, required)
208+
- `owner`: Repository owner (username or organization) (string, required)
209+
- `pullNumber`: The pull request number (number, required)
210+
- `repo`: Repository name (string, required)
211+
212+
- **update_pull_request_state** - Update Pull Request State
213+
- **Required OAuth Scopes**: `repo`
214+
- `owner`: Repository owner (username or organization) (string, required)
215+
- `pullNumber`: The pull request number (number, required)
216+
- `repo`: Repository name (string, required)
217+
- `state`: The new state for the pull request (string, required)
218+
219+
- **update_pull_request_title** - Update Pull Request Title
220+
- **Required OAuth Scopes**: `repo`
221+
- `owner`: Repository owner (username or organization) (string, required)
222+
- `pullNumber`: The pull request number (number, required)
223+
- `repo`: Repository name (string, required)
224+
- `title`: The new title for the pull request (string, required)
225+
<!-- END AUTOMATED FEATURE FLAG TOOLS -->

0 commit comments

Comments
 (0)