Skip to content

Commit f7d3a75

Browse files
Merge branch 'main' into add-logging-stack-v2
2 parents 39445b0 + bf64678 commit f7d3a75

File tree

10 files changed

+257
-105
lines changed

10 files changed

+257
-105
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ When no toolsets are specified, [default toolsets](#default-toolset) are used.
140140
</tr>
141141
</table>
142142

143-
See [Remote Server Documentation](docs/remote-server.md#insiders-mode) for more details and examples.
143+
See [Remote Server Documentation](docs/remote-server.md#insiders-mode) for more details and examples, and [Insiders Features](docs/insiders-features.md) for a full list of what's available.
144144

145145
#### GitHub Enterprise
146146

docs/insiders-features.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Insiders Features
2+
3+
Insiders Mode gives you access to experimental features in the GitHub MCP Server. These features may change, evolve, or be removed based on community feedback.
4+
5+
We created this mode to have a way to roll out experimental features and collect feedback. So if you are using Insiders, please don't hesitate to share your feedback with us!
6+
7+
> [!NOTE]
8+
> Features in Insiders Mode are experimental.
9+
10+
## Enabling Insiders Mode
11+
12+
| Method | Remote Server | Local Server |
13+
|--------|---------------|--------------|
14+
| URL path | Append `/insiders` to the URL | N/A |
15+
| Header | `X-MCP-Insiders: true` | N/A |
16+
| CLI flag | N/A | `--insiders` |
17+
| Environment variable | N/A | `GITHUB_INSIDERS=true` |
18+
19+
For configuration examples, see the [Server Configuration Guide](./server-configuration.md#insiders-mode).
20+
21+
---
22+
23+
## MCP Apps
24+
25+
[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that enables servers to deliver interactive user interfaces to end users. Instead of returning plain text that the LLM must interpret and relay, tools can render forms, profiles, and dashboards right in the chat using MCP Apps.
26+
27+
This means you can interact with GitHub visually: fill out forms to create issues, see user profiles with avatars, open pull requests — all without leaving your agent chat.
28+
29+
### Supported tools
30+
31+
The following tools have MCP Apps UIs:
32+
33+
| Tool | Description |
34+
|------|-------------|
35+
| `get_me` | Displays your GitHub user profile with avatar, bio, and stats in a rich card |
36+
| `issue_write` | Opens an interactive form to create or update issues |
37+
| `create_pull_request` | Provides a full PR creation form to create a pull request (or a draft pull request) |
38+
39+
### Client requirements
40+
41+
MCP Apps requires a host that supports the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps). Currently tested and working with:
42+
43+
- **VS Code Insiders** — enable via the `chat.mcp.apps.enabled` setting
44+
- **Visual Studio Code** — enable via the `chat.mcp.apps.enabled` setting

docs/server-configuration.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ We currently support the following ways in which the GitHub MCP Server can be co
1313
| Read-Only Mode | `X-MCP-Readonly` header or `/readonly` URL | `--read-only` flag or `GITHUB_READ_ONLY` env var |
1414
| Dynamic Mode | Not available | `--dynamic-toolsets` flag or `GITHUB_DYNAMIC_TOOLSETS` env var |
1515
| Lockdown Mode | `X-MCP-Lockdown` header | `--lockdown-mode` flag or `GITHUB_LOCKDOWN_MODE` env var |
16+
| Insiders Mode | `X-MCP-Insiders` header or `/insiders` URL | `--insiders` flag or `GITHUB_INSIDERS` env var |
1617
| Scope Filtering | Always enabled | Always enabled |
1718

1819
> **Default behavior:** If you don't specify any configuration, the server uses the **default toolsets**: `context`, `issues`, `pull_requests`, `repos`, `users`.
@@ -384,6 +385,63 @@ Lockdown mode ensures the server only surfaces content in public repositories fr
384385

385386
---
386387

388+
### Insiders Mode
389+
390+
**Best for:** Users who want early access to experimental features and new tools before they reach general availability.
391+
392+
Insiders Mode unlocks experimental features, such as [MCP Apps](./insiders-features.md#mcp-apps) support. We created this mode to have a way to roll out experimental features and collect feedback. So if you are using Insiders, please don't hesitate to share your feedback with us! Features in Insiders Mode may change, evolve, or be removed based on user feedback.
393+
394+
<table>
395+
<tr><th>Remote Server</th><th>Local Server</th></tr>
396+
<tr valign="top">
397+
<td>
398+
399+
**Option A: URL path**
400+
```json
401+
{
402+
"type": "http",
403+
"url": "https://api.githubcopilot.com/mcp/insiders"
404+
}
405+
```
406+
407+
**Option B: Header**
408+
```json
409+
{
410+
"type": "http",
411+
"url": "https://api.githubcopilot.com/mcp/",
412+
"headers": {
413+
"X-MCP-Insiders": "true"
414+
}
415+
}
416+
```
417+
418+
</td>
419+
<td>
420+
421+
```json
422+
{
423+
"type": "stdio",
424+
"command": "go",
425+
"args": [
426+
"run",
427+
"./cmd/github-mcp-server",
428+
"stdio",
429+
"--insiders"
430+
],
431+
"env": {
432+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
433+
}
434+
}
435+
```
436+
437+
</td>
438+
</tr>
439+
</table>
440+
441+
See [Insiders Features](./insiders-features.md) for a full list of what's available in Insiders Mode.
442+
443+
---
444+
387445
### Scope Filtering
388446

389447
**Automatic feature:** The server handles OAuth scopes differently depending on authentication type:

pkg/github/issues.go

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -201,33 +201,6 @@ func getIssueQueryType(hasLabels bool, hasSince bool) any {
201201
}
202202
}
203203

204-
func fragmentToIssue(fragment IssueFragment) *github.Issue {
205-
// Convert GraphQL labels to GitHub API labels format
206-
var foundLabels []*github.Label
207-
for _, labelNode := range fragment.Labels.Nodes {
208-
foundLabels = append(foundLabels, &github.Label{
209-
Name: github.Ptr(string(labelNode.Name)),
210-
NodeID: github.Ptr(string(labelNode.ID)),
211-
Description: github.Ptr(string(labelNode.Description)),
212-
})
213-
}
214-
215-
return &github.Issue{
216-
Number: github.Ptr(int(fragment.Number)),
217-
Title: github.Ptr(sanitize.Sanitize(string(fragment.Title))),
218-
CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time},
219-
UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time},
220-
User: &github.User{
221-
Login: github.Ptr(string(fragment.Author.Login)),
222-
},
223-
State: github.Ptr(string(fragment.State)),
224-
ID: github.Ptr(fragment.DatabaseID),
225-
Body: github.Ptr(sanitize.Sanitize(string(fragment.Body))),
226-
Labels: foundLabels,
227-
Comments: github.Ptr(int(fragment.Comments.TotalCount)),
228-
}
229-
}
230-
231204
// IssueRead creates a tool to get details of a specific issue in a GitHub repository.
232205
func IssueRead(t translations.TranslationHelperFunc) inventory.ServerTool {
233206
schema := &jsonschema.Schema{
@@ -1111,9 +1084,9 @@ Options are:
11111084
if numErr != nil {
11121085
return utils.NewToolResultError("issue_number is required for update method"), nil, nil
11131086
}
1114-
return utils.NewToolResultText(fmt.Sprintf("Ready to update issue #%d in %s/%s. The user will review and confirm via the interactive form.", issueNumber, owner, repo)), nil, nil
1087+
return utils.NewToolResultText(fmt.Sprintf("Ready to update issue #%d in %s/%s. IMPORTANT: The issue has NOT been updated yet. Do NOT tell the user the issue was updated. The user MUST click Submit in the form to update it.", issueNumber, owner, repo)), nil, nil
11151088
}
1116-
return utils.NewToolResultText(fmt.Sprintf("Ready to create an issue in %s/%s. The user will review and confirm via the interactive form.", owner, repo)), nil, nil
1089+
return utils.NewToolResultText(fmt.Sprintf("Ready to create an issue in %s/%s. IMPORTANT: The issue has NOT been created yet. Do NOT tell the user the issue was created. The user MUST click Submit in the form to create it.", owner, repo)), nil, nil
11171090
}
11181091

11191092
title, err := OptionalParam[string](args, "title")
@@ -1584,41 +1557,12 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
15841557
), nil, nil
15851558
}
15861559

1587-
// Extract and convert all issue nodes using the common interface
1588-
var issues []*github.Issue
1589-
var pageInfo struct {
1590-
HasNextPage githubv4.Boolean
1591-
HasPreviousPage githubv4.Boolean
1592-
StartCursor githubv4.String
1593-
EndCursor githubv4.String
1594-
}
1595-
var totalCount int
1596-
1560+
var resp MinimalIssuesResponse
15971561
if queryResult, ok := issueQuery.(IssueQueryResult); ok {
1598-
fragment := queryResult.GetIssueFragment()
1599-
for _, issue := range fragment.Nodes {
1600-
issues = append(issues, fragmentToIssue(issue))
1601-
}
1602-
pageInfo = fragment.PageInfo
1603-
totalCount = fragment.TotalCount
1604-
}
1605-
1606-
// Create response with issues
1607-
response := map[string]any{
1608-
"issues": issues,
1609-
"pageInfo": map[string]any{
1610-
"hasNextPage": pageInfo.HasNextPage,
1611-
"hasPreviousPage": pageInfo.HasPreviousPage,
1612-
"startCursor": string(pageInfo.StartCursor),
1613-
"endCursor": string(pageInfo.EndCursor),
1614-
},
1615-
"totalCount": totalCount,
1562+
resp = convertToMinimalIssuesResponse(queryResult.GetIssueFragment())
16161563
}
1617-
out, err := json.Marshal(response)
1618-
if err != nil {
1619-
return nil, nil, fmt.Errorf("failed to marshal issues: %w", err)
1620-
}
1621-
return utils.NewToolResultText(string(out)), nil, nil
1564+
1565+
return MarshalledTextResult(resp), nil, nil
16221566
})
16231567
}
16241568

pkg/github/issues_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,7 +1187,6 @@ func Test_ListIssues(t *testing.T) {
11871187
expectError bool
11881188
errContains string
11891189
expectedCount int
1190-
verifyOrder func(t *testing.T, issues []*github.Issue)
11911190
}{
11921191
{
11931192
name: "list all issues",
@@ -1296,31 +1295,32 @@ func Test_ListIssues(t *testing.T) {
12961295
require.NoError(t, err)
12971296

12981297
// Parse the structured response with pagination info
1299-
var response struct {
1300-
Issues []*github.Issue `json:"issues"`
1301-
PageInfo struct {
1302-
HasNextPage bool `json:"hasNextPage"`
1303-
HasPreviousPage bool `json:"hasPreviousPage"`
1304-
StartCursor string `json:"startCursor"`
1305-
EndCursor string `json:"endCursor"`
1306-
} `json:"pageInfo"`
1307-
TotalCount int `json:"totalCount"`
1308-
}
1298+
var response MinimalIssuesResponse
13091299
err = json.Unmarshal([]byte(text), &response)
13101300
require.NoError(t, err)
13111301

13121302
assert.Len(t, response.Issues, tc.expectedCount, "Expected %d issues, got %d", tc.expectedCount, len(response.Issues))
13131303

1314-
// Verify order if verifyOrder function is provided
1315-
if tc.verifyOrder != nil {
1316-
tc.verifyOrder(t, response.Issues)
1317-
}
1304+
// Verify pagination metadata
1305+
assert.Equal(t, tc.expectedCount, response.TotalCount)
1306+
assert.False(t, response.PageInfo.HasNextPage)
1307+
assert.False(t, response.PageInfo.HasPreviousPage)
13181308

13191309
// Verify that returned issues have expected structure
13201310
for _, issue := range response.Issues {
1321-
assert.NotNil(t, issue.Number, "Issue should have number")
1322-
assert.NotNil(t, issue.Title, "Issue should have title")
1323-
assert.NotNil(t, issue.State, "Issue should have state")
1311+
assert.NotZero(t, issue.Number, "Issue should have number")
1312+
assert.NotEmpty(t, issue.Title, "Issue should have title")
1313+
assert.NotEmpty(t, issue.State, "Issue should have state")
1314+
assert.NotEmpty(t, issue.CreatedAt, "Issue should have created_at")
1315+
assert.NotEmpty(t, issue.UpdatedAt, "Issue should have updated_at")
1316+
assert.NotNil(t, issue.User, "Issue should have user")
1317+
assert.NotEmpty(t, issue.User.Login, "Issue user should have login")
1318+
assert.Empty(t, issue.HTMLURL, "html_url should be empty (not populated by GraphQL fragment)")
1319+
1320+
// Labels should be flattened to name strings
1321+
for _, label := range issue.Labels {
1322+
assert.NotEmpty(t, label, "Label should be a non-empty string")
1323+
}
13241324
}
13251325
})
13261326
}

0 commit comments

Comments
 (0)