Skip to content

Commit 978915d

Browse files
Merge branch 'main' into dependabot/github_actions/actions/attest-build-provenance-4
2 parents b38629e + 486e9fe commit 978915d

File tree

9 files changed

+291
-9
lines changed

9 files changed

+291
-9
lines changed

.github/workflows/code-scanning.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ jobs:
3535
category: /language:go
3636
build-mode: autobuild
3737
runner: '["ubuntu-22.04"]'
38+
- language: javascript
39+
category: /language:javascript
40+
build-mode: none
41+
runner: '["ubuntu-22.04"]'
3842
steps:
3943
- name: Checkout repository
4044
uses: actions/checkout@v6
@@ -75,7 +79,7 @@ jobs:
7579
cache: false
7680

7781
- name: Set up Node.js
78-
if: matrix.language == 'go'
82+
if: matrix.language == 'go' || matrix.language == 'javascript'
7983
uses: actions/setup-node@v4
8084
with:
8185
node-version: "20"

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }}
9494

9595
- name: Inject go-build-cache
96-
uses: reproducible-containers/buildkit-cache-dance@6f699a72a59e4252f05a7435430009b77e25fe06 # v3.3.1
96+
uses: reproducible-containers/buildkit-cache-dance@1b8ab18fbda5ad3646e3fcc9ed9dd41ce2f297b4 # v3.3.2
9797
with:
9898
cache-map: |
9999
{

docs/installation-guides/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ This directory contains detailed installation instructions for the GitHub MCP Se
77
- **[GitHub Copilot in other IDEs](install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot
88
- **[Antigravity](install-antigravity.md)** - Installation for Google Antigravity IDE
99
- **[Claude Applications](install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI
10+
- **[Cline](install-cline.md)** - Installation guide for Cline
1011
- **[Cursor](install-cursor.md)** - Installation guide for Cursor IDE
1112
- **[Google Gemini CLI](install-gemini-cli.md)** - Installation guide for Google Gemini CLI
1213
- **[OpenAI Codex](install-codex.md)** - Installation guide for OpenAI Codex
14+
- **[Roo Code](install-roo-code.md)** - Installation guide for Roo Code
1315
- **[Windsurf](install-windsurf.md)** - Installation guide for Windsurf IDE
1416

1517
## Support by Host Application
@@ -23,8 +25,10 @@ This directory contains detailed installation instructions for the GitHub MCP Se
2325
| Copilot in JetBrains || ✅ Full (OAuth + PAT) | Local: Docker or Go build, GitHub PAT<br>Remote: JetBrains Copilot Extension v1.5.53+ | Easy |
2426
| Claude Code || ✅ PAT + ❌ No OAuth| GitHub MCP Server binary or remote URL, GitHub PAT | Easy |
2527
| Claude Desktop || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Moderate |
28+
| Cline || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
2629
| Cursor || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
2730
| Google Gemini CLI || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
31+
| Roo Code || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
2832
| Windsurf || ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
2933
| Copilot in Xcode || ✅ Full (OAuth + PAT) | Local: Docker or Go build, GitHub PAT<br>Remote: Copilot for Xcode 0.41.0+ | Easy |
3034
| Copilot in Eclipse || ✅ Full (OAuth + PAT) | Local: Docker or Go build, GitHub PAT<br>Remote: Eclipse Plug-in for Copilot 0.10.0+ | Easy |
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Install GitHub MCP Server in Cline
2+
3+
[Cline](https://github.com/cline/cline) is an AI coding assistant that runs in VS Code-compatible editors (VS Code, Cursor, Windsurf, etc.). For general setup information (prerequisites, Docker installation, security best practices), see the [Installation Guides README](./README.md).
4+
5+
## Remote Server
6+
7+
Cline stores MCP settings in `cline_mcp_settings.json`. To edit it, click the Cline icon in your editor's sidebar, open the menu in the top right corner of the Cline panel, and select **"MCP Servers"**. You can add a remote server through the **"Remote Servers"** tab, or click **"Configure MCP Servers"** to edit the JSON directly.
8+
9+
```json
10+
{
11+
"mcpServers": {
12+
"github": {
13+
"url": "https://api.githubcopilot.com/mcp/",
14+
"type": "streamableHttp",
15+
"disabled": false,
16+
"headers": {
17+
"Authorization": "Bearer <YOUR_GITHUB_PAT>"
18+
},
19+
"autoApprove": []
20+
}
21+
}
22+
}
23+
```
24+
25+
Replace `YOUR_GITHUB_PAT` with your [GitHub Personal Access Token](https://github.com/settings/tokens). To customize toolsets, add server-side headers like `X-MCP-Toolsets` or `X-MCP-Readonly` to the `headers` object — see [Server Configuration Guide](../server-configuration.md).
26+
27+
> **Important:** The transport type must be `"streamableHttp"` (camelCase, no hyphen). Using `"streamable-http"` or omitting the type will cause Cline to fall back to SSE, resulting in a `405` error.
28+
29+
## Local Server (Docker)
30+
31+
1. Click the Cline icon in your editor's sidebar (or open the command palette and search for "Cline"), then click the **MCP Servers** icon (server stack icon at the top of the Cline panel), and click **"Configure MCP Servers"** to open `cline_mcp_settings.json`.
32+
2. Add the configuration below, replacing `YOUR_GITHUB_PAT` with your [GitHub Personal Access Token](https://github.com/settings/tokens).
33+
34+
```json
35+
{
36+
"mcpServers": {
37+
"github": {
38+
"command": "docker",
39+
"args": [
40+
"run", "-i", "--rm",
41+
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
42+
"ghcr.io/github/github-mcp-server"
43+
],
44+
"env": {
45+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
46+
}
47+
}
48+
}
49+
}
50+
```
51+
52+
## Troubleshooting
53+
54+
- **SSE error 405 with remote server**: Ensure `"type"` is set to `"streamableHttp"` (camelCase, no hyphen) in `cline_mcp_settings.json`. Using `"streamable-http"` or omitting `"type"` causes Cline to fall back to SSE, which this server does not support.
55+
- **Authentication failures**: Verify your PAT has the required scopes
56+
- **Docker issues**: Ensure Docker Desktop is installed and running
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Install GitHub MCP Server in Roo Code
2+
3+
[Roo Code](https://github.com/RooCodeInc/Roo-Code) is an AI coding assistant that runs in VS Code-compatible editors (VS Code, Cursor, Windsurf, etc.). For general setup information (prerequisites, Docker installation, security best practices), see the [Installation Guides README](./README.md).
4+
5+
## Remote Server
6+
7+
### Step-by-step setup
8+
9+
1. Click the **Roo Code icon** in your editor's sidebar to open the Roo Code pane
10+
2. Click the **gear icon** (⚙️) in the top navigation of the Roo Code pane, then click on **"MCP Servers"** icon on the left.
11+
3. Scroll to the bottom and click **"Edit Global MCP"** (for all projects) or **"Edit Project MCP"** (for the current project only)
12+
4. Add the configuration below to the opened file (`mcp_settings.json` or `.roo/mcp.json`)
13+
5. Replace `YOUR_GITHUB_PAT` with your [GitHub Personal Access Token](https://github.com/settings/tokens)
14+
6. Save the file — the server should connect automatically
15+
16+
```json
17+
{
18+
"mcpServers": {
19+
"github": {
20+
"type": "streamable-http",
21+
"url": "https://api.githubcopilot.com/mcp/",
22+
"headers": {
23+
"Authorization": "Bearer YOUR_GITHUB_PAT"
24+
}
25+
}
26+
}
27+
}
28+
```
29+
30+
> **Important:** The `type` must be `"streamable-http"` (with hyphen). Using `"http"` or omitting the type will fail.
31+
32+
To customize toolsets, add server-side headers like `X-MCP-Toolsets` or `X-MCP-Readonly` to the `headers` object — see [Server Configuration Guide](../server-configuration.md).
33+
34+
## Local Server (Docker)
35+
36+
```json
37+
{
38+
"mcpServers": {
39+
"github": {
40+
"command": "docker",
41+
"args": [
42+
"run", "-i", "--rm",
43+
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
44+
"ghcr.io/github/github-mcp-server"
45+
],
46+
"env": {
47+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
48+
}
49+
}
50+
}
51+
}
52+
```
53+
54+
## Troubleshooting
55+
56+
- **Connection failures**: Ensure `type` is `streamable-http`, not `http`
57+
- **Authentication failures**: Verify PAT is prefixed with `Bearer ` in the `Authorization` header
58+
- **Docker issues**: Ensure Docker Desktop is running

pkg/github/issues.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func fetchIssueIDs(ctx context.Context, gqlClient *githubv4.Client, owner, repo
6262
}
6363

6464
if err := gqlClient.Query(ctx, &query, vars); err != nil {
65-
return "", "", fmt.Errorf("failed to get issue ID")
65+
return "", "", fmt.Errorf("failed to get issue ID: %w", err)
6666
}
6767

6868
return query.Repository.Issue.ID, "", nil
@@ -84,7 +84,7 @@ func fetchIssueIDs(ctx context.Context, gqlClient *githubv4.Client, owner, repo
8484
vars["duplicateOf"] = githubv4.Int(duplicateOf) // #nosec G115 - issue numbers are always small positive integers
8585

8686
if err := gqlClient.Query(ctx, &query, vars); err != nil {
87-
return "", "", fmt.Errorf("failed to get issue ID")
87+
return "", "", fmt.Errorf("failed to get issue ID: %w", err)
8888
}
8989

9090
return query.Repository.Issue.ID, query.Repository.DuplicateIssue.ID, nil
@@ -1080,13 +1080,19 @@ Options are:
10801080

10811081
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
10821082
if method == "update" {
1083-
issueNumber, numErr := RequiredInt(args, "issue_number")
1084-
if numErr != nil {
1085-
return utils.NewToolResultError("issue_number is required for update method"), nil, nil
1083+
// Skip the UI form when a state change is requested because
1084+
// the form only handles title/body editing and would lose the
1085+
// state transition (e.g. closing or reopening the issue).
1086+
if _, hasState := args["state"]; !hasState {
1087+
issueNumber, numErr := RequiredInt(args, "issue_number")
1088+
if numErr != nil {
1089+
return utils.NewToolResultError("issue_number is required for update method"), nil, nil
1090+
}
1091+
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
10861092
}
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
1093+
} else {
1094+
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
10881095
}
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
10901096
}
10911097

10921098
title, err := OptionalParam[string](args, "title")

pkg/github/issues_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,112 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
10001000
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
10011001
"non-UI client should execute directly")
10021002
})
1003+
1004+
t.Run("UI client with state change skips form and executes directly", func(t *testing.T) {
1005+
mockBaseIssue := &github.Issue{
1006+
Number: github.Ptr(1),
1007+
Title: github.Ptr("Test"),
1008+
State: github.Ptr("open"),
1009+
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1"),
1010+
}
1011+
issueIDQueryResponse := githubv4mock.DataResponse(map[string]any{
1012+
"repository": map[string]any{
1013+
"issue": map[string]any{
1014+
"id": "I_kwDOA0xdyM50BPaO",
1015+
},
1016+
},
1017+
})
1018+
closeSuccessResponse := githubv4mock.DataResponse(map[string]any{
1019+
"closeIssue": map[string]any{
1020+
"issue": map[string]any{
1021+
"id": "I_kwDOA0xdyM50BPaO",
1022+
"number": 1,
1023+
"url": "https://github.com/owner/repo/issues/1",
1024+
"state": "CLOSED",
1025+
},
1026+
},
1027+
})
1028+
completedReason := IssueClosedStateReasonCompleted
1029+
1030+
closeClient := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1031+
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
1032+
}))
1033+
closeGQLClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(
1034+
githubv4mock.NewQueryMatcher(
1035+
struct {
1036+
Repository struct {
1037+
Issue struct {
1038+
ID githubv4.ID
1039+
} `graphql:"issue(number: $issueNumber)"`
1040+
} `graphql:"repository(owner: $owner, name: $repo)"`
1041+
}{},
1042+
map[string]any{
1043+
"owner": githubv4.String("owner"),
1044+
"repo": githubv4.String("repo"),
1045+
"issueNumber": githubv4.Int(1),
1046+
},
1047+
issueIDQueryResponse,
1048+
),
1049+
githubv4mock.NewMutationMatcher(
1050+
struct {
1051+
CloseIssue struct {
1052+
Issue struct {
1053+
ID githubv4.ID
1054+
Number githubv4.Int
1055+
URL githubv4.String
1056+
State githubv4.String
1057+
}
1058+
} `graphql:"closeIssue(input: $input)"`
1059+
}{},
1060+
CloseIssueInput{
1061+
IssueID: "I_kwDOA0xdyM50BPaO",
1062+
StateReason: &completedReason,
1063+
},
1064+
nil,
1065+
closeSuccessResponse,
1066+
),
1067+
))
1068+
1069+
closeDeps := BaseDeps{
1070+
Client: closeClient,
1071+
GQLClient: closeGQLClient,
1072+
Flags: FeatureFlags{InsidersMode: true},
1073+
}
1074+
closeHandler := serverTool.Handler(closeDeps)
1075+
1076+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
1077+
"method": "update",
1078+
"owner": "owner",
1079+
"repo": "repo",
1080+
"issue_number": float64(1),
1081+
"state": "closed",
1082+
"state_reason": "completed",
1083+
})
1084+
result, err := closeHandler(ContextWithDeps(context.Background(), closeDeps), &request)
1085+
require.NoError(t, err)
1086+
1087+
textContent := getTextResult(t, result)
1088+
assert.NotContains(t, textContent.Text, "Ready to update issue",
1089+
"state change should skip UI form")
1090+
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1091+
"state change should execute directly and return issue URL")
1092+
})
1093+
1094+
t.Run("UI client update without state change returns form message", func(t *testing.T) {
1095+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
1096+
"method": "update",
1097+
"owner": "owner",
1098+
"repo": "repo",
1099+
"issue_number": float64(1),
1100+
"title": "New Title",
1101+
})
1102+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1103+
require.NoError(t, err)
1104+
1105+
textContent := getTextResult(t, result)
1106+
assert.Contains(t, textContent.Text, "Ready to update issue #1",
1107+
"update without state should show UI form")
1108+
})
10031109
}
10041110

10051111
func Test_ListIssues(t *testing.T) {

pkg/github/repositories.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,20 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
726726
successNote = fmt.Sprintf(" Note: the provided ref '%s' does not exist, default branch '%s' was used instead.", originalRef, rawOpts.Ref)
727727
}
728728

729+
// Empty files (0 bytes) have no content to decode; return
730+
// them directly as empty text to avoid errors from
731+
// GetContent when the API returns null content with a
732+
// base64 encoding field, and to avoid DetectContentType
733+
// misclassifying them as binary.
734+
if fileSize == 0 {
735+
result := &mcp.ResourceContents{
736+
URI: resourceURI,
737+
Text: "",
738+
MIMEType: "text/plain",
739+
}
740+
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded empty file (SHA: %s)%s", fileSHA, successNote), result), nil, nil
741+
}
742+
729743
// For files >= 1MB, return a ResourceLink instead of content
730744
const maxContentSize = 1024 * 1024 // 1MB
731745
if fileSize >= maxContentSize {

pkg/github/repositories_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,40 @@ func Test_GetFileContents(t *testing.T) {
351351
Title: "File: large-file.bin",
352352
},
353353
},
354+
{
355+
name: "successful empty file content fetch",
356+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
357+
GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"),
358+
GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"),
359+
GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) {
360+
w.WriteHeader(http.StatusOK)
361+
fileContent := &github.RepositoryContent{
362+
Name: github.Ptr(".gitkeep"),
363+
Path: github.Ptr(".gitkeep"),
364+
SHA: github.Ptr("empty123"),
365+
Type: github.Ptr("file"),
366+
Content: nil,
367+
Size: github.Ptr(0),
368+
Encoding: github.Ptr("base64"),
369+
}
370+
contentBytes, _ := json.Marshal(fileContent)
371+
_, _ = w.Write(contentBytes)
372+
},
373+
}),
374+
requestArgs: map[string]any{
375+
"owner": "owner",
376+
"repo": "repo",
377+
"path": ".gitkeep",
378+
"ref": "refs/heads/main",
379+
},
380+
expectError: false,
381+
expectedResult: mcp.ResourceContents{
382+
URI: "repo://owner/repo/refs/heads/main/contents/.gitkeep",
383+
Text: "",
384+
MIMEType: "text/plain",
385+
},
386+
expectedMsg: "successfully downloaded empty file",
387+
},
354388
{
355389
name: "content fetch fails",
356390
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{

0 commit comments

Comments
 (0)