Skip to content

Commit cac823c

Browse files
committed
Merge branch 'tmelliottjr/update-projects-tools' into tmelliottjr/update-projects-descriptions
2 parents 8c306ae + da66054 commit cac823c

18 files changed

Lines changed: 688 additions & 241 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ The following sets of tools are available:
400400
| `discussions` | GitHub Discussions related tools |
401401
| `experiments` | Experimental features that are not considered stable yet |
402402
| `gists` | GitHub Gist related tools |
403+
| `git` | GitHub Git API related tools for low-level Git operations |
403404
| `issues` | GitHub Issues related tools |
404405
| `labels` | GitHub Labels related tools |
405406
| `notifications` | GitHub Notifications related tools |
@@ -630,6 +631,19 @@ The following sets of tools are available:
630631

631632
<details>
632633

634+
<summary>Git</summary>
635+
636+
- **get_repository_tree** - Get repository tree
637+
- `owner`: Repository owner (username or organization) (string, required)
638+
- `path_filter`: Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory) (string, optional)
639+
- `recursive`: Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false (boolean, optional)
640+
- `repo`: Repository name (string, required)
641+
- `tree_sha`: The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch (string, optional)
642+
643+
</details>
644+
645+
<details>
646+
633647
<summary>Issues</summary>
634648

635649
- **add_issue_comment** - Add comment to issue

cmd/github-mcp-server/generate_docs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func generateReadmeDocs(readmePath string) error {
6464
t, _ := translations.TranslationHelper()
6565

6666
// Create toolset group with mock clients
67-
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
67+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{})
6868

6969
// Generate toolsets documentation
7070
toolsetsDoc := generateToolsetsDoc(tsg)
@@ -302,7 +302,7 @@ func generateRemoteToolsetsDoc() string {
302302
t, _ := translations.TranslationHelper()
303303

304304
// Create toolset group with mock clients
305-
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
305+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{})
306306

307307
// Generate table header
308308
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")

cmd/github-mcp-server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ var (
6161
EnableCommandLogging: viper.GetBool("enable-command-logging"),
6262
LogFilePath: viper.GetString("log-file"),
6363
ContentWindowSize: viper.GetInt("content-window-size"),
64+
LockdownMode: viper.GetBool("lockdown-mode"),
6465
}
6566
return ghmcp.RunStdioServer(stdioServerConfig)
6667
},
@@ -82,6 +83,7 @@ func init() {
8283
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
8384
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
8485
rootCmd.PersistentFlags().Int("content-window-size", 5000, "Specify the content window size")
86+
rootCmd.PersistentFlags().Bool("lockdown-mode", false, "Enable lockdown mode")
8587

8688
// Bind flag to viper
8789
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -92,6 +94,7 @@ func init() {
9294
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
9395
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
9496
_ = viper.BindPFlag("content-window-size", rootCmd.PersistentFlags().Lookup("content-window-size"))
97+
_ = viper.BindPFlag("lockdown-mode", rootCmd.PersistentFlags().Lookup("lockdown-mode"))
9598

9699
// Add subcommands
97100
rootCmd.AddCommand(stdioCmd)

cmd/mcpcurl/mcpcurl

6.41 MB
Binary file not shown.

docs/remote-server.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
2626
| Discussions | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) |
2727
| Experiments | Experimental features that are not considered stable yet | https://api.githubcopilot.com/mcp/x/experiments | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/experiments/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%2Freadonly%22%7D) |
2828
| Gists | GitHub Gist related tools | https://api.githubcopilot.com/mcp/x/gists | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/gists/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%2Freadonly%22%7D) |
29+
| Git | GitHub Git API related tools for low-level Git operations | https://api.githubcopilot.com/mcp/x/git | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-git&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgit%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/git/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-git&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgit%2Freadonly%22%7D) |
2930
| Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
3031
| Labels | GitHub Labels related tools | https://api.githubcopilot.com/mcp/x/labels | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/labels/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%2Freadonly%22%7D) |
3132
| Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) |

internal/ghmcp/server.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ type MCPServerConfig struct {
5151

5252
// Content window size
5353
ContentWindowSize int
54+
55+
// LockdownMode indicates if we should enable lockdown mode
56+
LockdownMode bool
5457
}
5558

5659
const stdioServerLogPrefix = "stdioserver"
@@ -154,7 +157,15 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
154157
}
155158

156159
// Create default toolsets
157-
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator, cfg.ContentWindowSize)
160+
tsg := github.DefaultToolsetGroup(
161+
cfg.ReadOnly,
162+
getClient,
163+
getGQLClient,
164+
getRawClient,
165+
cfg.Translator,
166+
cfg.ContentWindowSize,
167+
github.FeatureFlags{LockdownMode: cfg.LockdownMode},
168+
)
158169
err = tsg.EnableToolsets(enabledToolsets, nil)
159170

160171
if err != nil {
@@ -205,6 +216,9 @@ type StdioServerConfig struct {
205216

206217
// Content window size
207218
ContentWindowSize int
219+
220+
// LockdownMode indicates if we should enable lockdown mode
221+
LockdownMode bool
208222
}
209223

210224
// RunStdioServer is not concurrent safe.
@@ -224,6 +238,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
224238
ReadOnly: cfg.ReadOnly,
225239
Translator: t,
226240
ContentWindowSize: cfg.ContentWindowSize,
241+
LockdownMode: cfg.LockdownMode,
227242
})
228243
if err != nil {
229244
return fmt.Errorf("failed to create MCP server: %w", err)
@@ -245,7 +260,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
245260
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
246261
}
247262
logger := slog.New(slogHandler)
248-
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly)
263+
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
249264
stdLogger := log.New(logOutput, stdioServerLogPrefix, 0)
250265
stdioServer.SetErrorLogger(stdLogger)
251266

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"annotations": {
3+
"title": "Get repository tree",
4+
"readOnlyHint": true
5+
},
6+
"description": "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "Repository owner (username or organization)",
11+
"type": "string"
12+
},
13+
"path_filter": {
14+
"description": "Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory)",
15+
"type": "string"
16+
},
17+
"recursive": {
18+
"default": false,
19+
"description": "Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false",
20+
"type": "boolean"
21+
},
22+
"repo": {
23+
"description": "Repository name",
24+
"type": "string"
25+
},
26+
"tree_sha": {
27+
"description": "The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch",
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"owner",
33+
"repo"
34+
],
35+
"type": "object"
36+
},
37+
"name": "get_repository_tree"
38+
}

pkg/github/feature_flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package github
2+
3+
// FeatureFlags defines runtime feature toggles that adjust tool behavior.
4+
type FeatureFlags struct {
5+
LockdownMode bool
6+
}

pkg/github/git.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
ghErrors "github.com/github/github-mcp-server/pkg/errors"
10+
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/google/go-github/v77/github"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
// TreeEntryResponse represents a single entry in a Git tree.
17+
type TreeEntryResponse struct {
18+
Path string `json:"path"`
19+
Type string `json:"type"`
20+
Size *int `json:"size,omitempty"`
21+
Mode string `json:"mode"`
22+
SHA string `json:"sha"`
23+
URL string `json:"url"`
24+
}
25+
26+
// TreeResponse represents the response structure for a Git tree.
27+
type TreeResponse struct {
28+
SHA string `json:"sha"`
29+
Truncated bool `json:"truncated"`
30+
Tree []TreeEntryResponse `json:"tree"`
31+
TreeSHA string `json:"tree_sha"`
32+
Owner string `json:"owner"`
33+
Repo string `json:"repo"`
34+
Recursive bool `json:"recursive"`
35+
Count int `json:"count"`
36+
}
37+
38+
// GetRepositoryTree creates a tool to get the tree structure of a GitHub repository.
39+
func GetRepositoryTree(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
40+
return mcp.NewTool("get_repository_tree",
41+
mcp.WithDescription(t("TOOL_GET_REPOSITORY_TREE_DESCRIPTION", "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA")),
42+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
43+
Title: t("TOOL_GET_REPOSITORY_TREE_USER_TITLE", "Get repository tree"),
44+
ReadOnlyHint: ToBoolPtr(true),
45+
}),
46+
mcp.WithString("owner",
47+
mcp.Required(),
48+
mcp.Description("Repository owner (username or organization)"),
49+
),
50+
mcp.WithString("repo",
51+
mcp.Required(),
52+
mcp.Description("Repository name"),
53+
),
54+
mcp.WithString("tree_sha",
55+
mcp.Description("The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch"),
56+
),
57+
mcp.WithBoolean("recursive",
58+
mcp.Description("Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false"),
59+
mcp.DefaultBool(false),
60+
),
61+
mcp.WithString("path_filter",
62+
mcp.Description("Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory)"),
63+
),
64+
),
65+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
66+
owner, err := RequiredParam[string](request, "owner")
67+
if err != nil {
68+
return mcp.NewToolResultError(err.Error()), nil
69+
}
70+
repo, err := RequiredParam[string](request, "repo")
71+
if err != nil {
72+
return mcp.NewToolResultError(err.Error()), nil
73+
}
74+
treeSHA, err := OptionalParam[string](request, "tree_sha")
75+
if err != nil {
76+
return mcp.NewToolResultError(err.Error()), nil
77+
}
78+
recursive, err := OptionalBoolParamWithDefault(request, "recursive", false)
79+
if err != nil {
80+
return mcp.NewToolResultError(err.Error()), nil
81+
}
82+
pathFilter, err := OptionalParam[string](request, "path_filter")
83+
if err != nil {
84+
return mcp.NewToolResultError(err.Error()), nil
85+
}
86+
87+
client, err := getClient(ctx)
88+
if err != nil {
89+
return mcp.NewToolResultError("failed to get GitHub client"), nil
90+
}
91+
92+
// If no tree_sha is provided, use the repository's default branch
93+
if treeSHA == "" {
94+
repoInfo, repoResp, err := client.Repositories.Get(ctx, owner, repo)
95+
if err != nil {
96+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
97+
"failed to get repository info",
98+
repoResp,
99+
err,
100+
), nil
101+
}
102+
treeSHA = *repoInfo.DefaultBranch
103+
}
104+
105+
// Get the tree using the GitHub Git Tree API
106+
tree, resp, err := client.Git.GetTree(ctx, owner, repo, treeSHA, recursive)
107+
if err != nil {
108+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
109+
"failed to get repository tree",
110+
resp,
111+
err,
112+
), nil
113+
}
114+
defer func() { _ = resp.Body.Close() }()
115+
116+
// Filter tree entries if path_filter is provided
117+
var filteredEntries []*github.TreeEntry
118+
if pathFilter != "" {
119+
for _, entry := range tree.Entries {
120+
if strings.HasPrefix(entry.GetPath(), pathFilter) {
121+
filteredEntries = append(filteredEntries, entry)
122+
}
123+
}
124+
} else {
125+
filteredEntries = tree.Entries
126+
}
127+
128+
treeEntries := make([]TreeEntryResponse, len(filteredEntries))
129+
for i, entry := range filteredEntries {
130+
treeEntries[i] = TreeEntryResponse{
131+
Path: entry.GetPath(),
132+
Type: entry.GetType(),
133+
Mode: entry.GetMode(),
134+
SHA: entry.GetSHA(),
135+
URL: entry.GetURL(),
136+
}
137+
if entry.Size != nil {
138+
treeEntries[i].Size = entry.Size
139+
}
140+
}
141+
142+
response := TreeResponse{
143+
SHA: *tree.SHA,
144+
Truncated: *tree.Truncated,
145+
Tree: treeEntries,
146+
TreeSHA: treeSHA,
147+
Owner: owner,
148+
Repo: repo,
149+
Recursive: recursive,
150+
Count: len(filteredEntries),
151+
}
152+
153+
r, err := json.Marshal(response)
154+
if err != nil {
155+
return nil, fmt.Errorf("failed to marshal response: %w", err)
156+
}
157+
158+
return mcp.NewToolResultText(string(r)), nil
159+
}
160+
}

0 commit comments

Comments
 (0)