Skip to content

Commit c6ce146

Browse files
Merge branch 'main' into iunia/reapply-issue-field-commits
2 parents e446797 + f929c58 commit c6ce146

44 files changed

Lines changed: 2990 additions & 352 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/mcp-diff.yml

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,35 @@ jobs:
1919
with:
2020
fetch-depth: 0
2121

22+
- name: Set up Go
23+
uses: actions/setup-go@v5
24+
with:
25+
go-version-file: go.mod
26+
2227
- name: Build UI
2328
uses: ./.github/actions/build-ui
2429

30+
- name: Generate diff configurations
31+
id: configs
32+
# The generator imports pkg/github so any new entry in
33+
# AllowedFeatureFlags is automatically diffed without touching this
34+
# workflow. See script/print-mcp-diff-configs/main.go.
35+
run: |
36+
{
37+
echo 'configurations<<MCP_DIFF_EOF'
38+
go run ./script/print-mcp-diff-configs
39+
echo 'MCP_DIFF_EOF'
40+
} >> "$GITHUB_OUTPUT"
41+
2542
- name: Run MCP Server Diff
2643
uses: SamMorrowDrums/mcp-server-diff@v2.3.5
2744
with:
28-
setup_go: "true"
45+
setup_go: "false"
2946
install_command: go mod download
3047
start_command: go run ./cmd/github-mcp-server stdio
3148
env_vars: |
3249
GITHUB_PERSONAL_ACCESS_TOKEN=test-token
33-
configurations: |
34-
[
35-
{"name": "default", "args": ""},
36-
{"name": "read-only", "args": "--read-only"},
37-
{"name": "toolsets-repos", "args": "--toolsets=repos"},
38-
{"name": "toolsets-issues", "args": "--toolsets=issues"},
39-
{"name": "toolsets-context", "args": "--toolsets=context"},
40-
{"name": "toolsets-pull_requests", "args": "--toolsets=pull_requests"},
41-
{"name": "toolsets-repos,issues", "args": "--toolsets=repos,issues"},
42-
{"name": "toolsets-issues,context", "args": "--toolsets=issues,context"},
43-
{"name": "toolsets-all", "args": "--toolsets=all"},
44-
{"name": "tools-get_me", "args": "--tools=get_me"},
45-
{"name": "tools-get_me,list_issues", "args": "--tools=get_me,list_issues"},
46-
{"name": "toolsets-repos+read-only", "args": "--toolsets=repos --read-only"}
47-
]
50+
configurations: ${{ steps.configs.outputs.configurations }}
4851

4952
- name: Add interpretation note
5053
if: always()
@@ -58,3 +61,51 @@ jobs:
5861
echo "- New tools/toolsets added" >> $GITHUB_STEP_SUMMARY
5962
echo "- Tool descriptions updated" >> $GITHUB_STEP_SUMMARY
6063
echo "- Capability changes (intentional improvements)" >> $GITHUB_STEP_SUMMARY
64+
65+
mcp-diff-http:
66+
runs-on: ubuntu-latest
67+
68+
steps:
69+
- name: Check out code
70+
uses: actions/checkout@v6
71+
with:
72+
fetch-depth: 0
73+
74+
- name: Set up Go
75+
uses: actions/setup-go@v5
76+
with:
77+
go-version-file: go.mod
78+
79+
- name: Build UI
80+
uses: ./.github/actions/build-ui
81+
82+
- name: Generate diff configurations
83+
id: configs
84+
# See script/print-mcp-diff-configs/main.go. The http-headers variant
85+
# points every config at a shared HTTP server started by the action
86+
# and carries per-config settings via X-MCP-* headers, mirroring how
87+
# the remote server is invoked in production (server-side defaults +
88+
# per-user header overrides).
89+
run: |
90+
{
91+
echo 'configurations<<MCP_DIFF_EOF'
92+
go run ./script/print-mcp-diff-configs -transport http-headers
93+
echo 'MCP_DIFF_EOF'
94+
} >> "$GITHUB_OUTPUT"
95+
96+
- name: Run MCP Server Diff (streamable-http)
97+
uses: SamMorrowDrums/mcp-server-diff@v2.3.5
98+
with:
99+
setup_go: "false"
100+
install_command: go mod download
101+
http_start_command: go run ./cmd/github-mcp-server http --port 8082
102+
http_startup_wait_ms: "5000"
103+
configurations: ${{ steps.configs.outputs.configurations }}
104+
105+
- name: Add interpretation note
106+
if: always()
107+
run: |
108+
echo "" >> $GITHUB_STEP_SUMMARY
109+
echo "---" >> $GITHUB_STEP_SUMMARY
110+
echo "" >> $GITHUB_STEP_SUMMARY
111+
echo "ℹ️ **Note:** This job exercises the streamable-http transport against a shared server, with per-config settings supplied via X-MCP-* request headers." >> $GITHUB_STEP_SUMMARY

README.md

Lines changed: 180 additions & 5 deletions
Large diffs are not rendered by default.

cmd/github-mcp-server/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ var (
126126
}
127127
}
128128

129+
var enabledFeatures []string
130+
if viper.IsSet("features") {
131+
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
132+
return fmt.Errorf("failed to unmarshal features: %w", err)
133+
}
134+
}
135+
129136
ttl := viper.GetDuration("repo-access-cache-ttl")
130137
httpConfig := ghhttp.ServerConfig{
131138
Version: version,
@@ -144,6 +151,7 @@ var (
144151
EnabledToolsets: enabledToolsets,
145152
EnabledTools: enabledTools,
146153
ExcludeTools: excludeTools,
154+
EnabledFeatures: enabledFeatures,
147155
InsidersMode: viper.GetBool("insiders"),
148156
}
149157

docs/insiders-features.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,60 @@ MCP Apps requires a host that supports the [MCP Apps extension](https://modelcon
4242

4343
- **VS Code Insiders** — enable via the `chat.mcp.apps.enabled` setting
4444
- **Visual Studio Code** — enable via the `chat.mcp.apps.enabled` setting
45+
46+
---
47+
48+
## CSV output for list tools
49+
50+
CSV output mode returns supported list tool responses as CSV instead of JSON. This is intended to reduce response context for agents when scanning or summarising lists of GitHub data.
51+
52+
CSV output applies only to tools in default toolsets whose names start with `list_`, such as `list_issues`, `list_pull_requests`, `list_commits`, and `list_branches`. It does not add new tools or expose a tool argument for selecting the format; the server controls the response format through the Insiders feature flag.
53+
54+
### Format
55+
56+
- Nested objects are flattened into dot-notation columns, for example `user.login`, `category.name`, or `head.ref`.
57+
- Arrays are represented as compact single-cell values joined with `;`.
58+
- `body` fields are whitespace-normalized so multiline Markdown does not expand a list response into many output lines.
59+
- Response metadata present in wrapped responses, such as `pageInfo.*` and `totalCount`, is emitted as `#`-prefixed lines before the CSV rows, followed by a blank line. Tools that return a root JSON array do not include metadata preamble lines.
60+
61+
### Enabling CSV output
62+
63+
CSV output is enabled by Insiders Mode. For local development, it can also be enabled explicitly with the `csv_output` feature flag:
64+
65+
```bash
66+
github-mcp-server stdio --features csv_output
67+
```
68+
69+
Because this changes list tool response shape, clients that require JSON list responses should avoid enabling this feature.
70+
71+
---
72+
73+
## How feature flags are resolved
74+
75+
> [!NOTE]
76+
> This section is for contributors. End users only need the table at the top of this page.
77+
78+
Insiders is a **meta feature flag** — the same shape as `default` or `all` for toolsets. It expands once at startup into a curated set of individual feature flags, and from that point on every code path keys off concrete flags, never `InsidersMode` directly. New experimental work should always get its own flag and then be added to the insiders expansion list, never folded into `insiders` as a catch-all.
79+
80+
### Resolution order
81+
82+
1. **User input.** Users may opt into specific features:
83+
- Local server: `--features=<flag>,<flag>` CLI flag (or `GITHUB_FEATURES` env var).
84+
- Self-hosted HTTP server: `X-MCP-Features: <flag>,<flag>` request header.
85+
2. **Allowlist filter.** User-supplied flags are filtered against [`AllowedFeatureFlags`](../pkg/github/feature_flags.go). Anything not on the allowlist is silently dropped — flags missing from the allowlist can only be turned on by remote-server feature management, not by end users.
86+
3. **Insiders expansion.** If insiders mode is on (`--insiders`, `/insiders` route, or `X-MCP-Insiders: true`), every flag in [`InsidersFeatureFlags`](../pkg/github/feature_flags.go) is unioned in. The insiders expansion is **not** re-validated against the allowlist — insiders is a server-controlled switch that can reach internal-only flags.
87+
4. **Server-side fallback (remote server only).** Any flag not yet decided falls back to the remote server's feature manager, which can roll a feature out independently of user input or insiders membership.
88+
89+
`AllowedFeatureFlags` and `InsidersFeatureFlags` are deliberately independent sets:
90+
91+
- A flag in **`AllowedFeatureFlags` only** is a regular opt-in: users can turn it on, but insiders does not auto-enable it. Granular issues/PRs flags work this way.
92+
- A flag in **`InsidersFeatureFlags` only** is reachable through insiders (and remote-server rollouts), but cannot be enabled by user input. Internal-only experiments work this way.
93+
- A flag in **both** is opt-in for end users *and* automatically on under insiders.
94+
95+
### Adding a new feature flag
96+
97+
1. Add a constant in `pkg/github/feature_flags.go`.
98+
2. Add it to `AllowedFeatureFlags` if end users should be able to opt in via `--features` / `X-MCP-Features`.
99+
3. Add it to `InsidersFeatureFlags` if insiders mode should turn it on automatically.
100+
4. Gate the behavior on the concrete flag (`deps.IsFeatureEnabled(ctx, FeatureFlagX)`), never on `cfg.InsidersMode`. There is a `TestGitHubPackageDoesNotReadInsidersMode` guard test that fails if `pkg/github` reads `InsidersMode` directly.
101+
5. The MCP-diff CI workflow picks up new entries in `AllowedFeatureFlags` automatically — see `.github/workflows/mcp-diff.yml`.

internal/ghmcp/server.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
143143
cfg.Translator,
144144
github.FeatureFlags{
145145
LockdownMode: cfg.LockdownMode,
146-
InsidersMode: cfg.InsidersMode,
147146
},
148147
cfg.ContentWindowSize,
149148
featureChecker,
@@ -229,7 +228,7 @@ type StdioServerConfig struct {
229228
// LockdownMode indicates if we should enable lockdown mode
230229
LockdownMode bool
231230

232-
// InsidersMode indicates if we should enable experimental features
231+
// InsidersMode expands to the curated set of feature flags enabled for insiders.
233232
InsidersMode bool
234233

235234
// ExcludeTools is a list of tool names to disable regardless of other settings.
@@ -345,7 +344,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
345344

346345
// createFeatureChecker returns a FeatureFlagChecker that resolves features
347346
// using the centralized ResolveFeatureFlags function. For the local server,
348-
// features are resolved once at startup from --features CLI flag + insiders mode.
347+
// features are resolved once at startup from --features CLI flag and insiders mode.
349348
func createFeatureChecker(enabledFeatures []string, insidersMode bool) inventory.FeatureFlagChecker {
350349
featureSet := github.ResolveFeatureFlags(enabledFeatures, insidersMode)
351350
return func(_ context.Context, flagName string) (bool, error) {

pkg/github/__toolsnaps__/get_label.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"annotations": {
33
"readOnlyHint": true,
4-
"title": "Get a specific label from a repository."
4+
"title": "Get a specific label from a repository"
55
},
66
"description": "Get a specific label from a repository.",
77
"inputSchema": {

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010
},
1111
"annotations": {
12-
"title": "Create or update issue."
12+
"title": "Create or update issue"
1313
},
1414
"description": "Create a new or update an existing issue in a GitHub repository.",
1515
"inputSchema": {

pkg/github/__toolsnaps__/label_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"annotations": {
3-
"title": "Write operations on repository labels."
3+
"title": "Write operations on repository labels"
44
},
55
"description": "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.",
66
"inputSchema": {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "List issue fields"
5+
},
6+
"description": "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select), and for single_select fields the list of valid option names. When repo is omitted, returns org-level fields directly.",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "The account owner of the repository or organization. The name is not case sensitive.",
11+
"type": "string"
12+
},
13+
"repo": {
14+
"description": "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"owner"
20+
],
21+
"type": "object"
22+
},
23+
"name": "list_issue_fields"
24+
}

pkg/github/__toolsnaps__/list_issues.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
],
1919
"type": "string"
2020
},
21+
"field_filters": {
22+
"description": "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).",
23+
"items": {
24+
"properties": {
25+
"field_name": {
26+
"description": "Name of the custom field (e.g. \"Priority\"). Case-insensitive.",
27+
"type": "string"
28+
},
29+
"value": {
30+
"description": "Value to filter on. For single-select fields, the option name (e.g. \"P1\"). For dates, YYYY-MM-DD. For numbers, the numeric value as a string. For text, the text value.",
31+
"type": "string"
32+
}
33+
},
34+
"required": [
35+
"field_name",
36+
"value"
37+
],
38+
"type": "object"
39+
},
40+
"type": "array"
41+
},
2142
"labels": {
2243
"description": "Filter by labels",
2344
"items": {

0 commit comments

Comments
 (0)