Skip to content

Commit 2791676

Browse files
authored
Merge branch 'main' into kerobbi/response-optimisation
2 parents c75ab7d + dc3ee11 commit 2791676

File tree

14 files changed

+355
-9
lines changed

14 files changed

+355
-9
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:20-alpine AS ui-build
1+
FROM node:20-alpine@sha256:09e2b3d9726018aecf269bd35325f46bf75046a643a66d28360ec71132750ec8 AS ui-build
22
WORKDIR /app
33
COPY ui/package*.json ./ui/
44
RUN cd ui && npm ci
@@ -7,7 +7,7 @@ COPY ui/ ./ui/
77
RUN mkdir -p ./pkg/github/ui_dist && \
88
cd ui && npm run build
99

10-
FROM golang:1.25.7-alpine AS build
10+
FROM golang:1.25.7-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS build
1111
ARG VERSION="dev"
1212

1313
# Set the working directory
@@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
3030
-o /bin/github-mcp-server ./cmd/github-mcp-server
3131

3232
# Make a stage to run the app
33-
FROM gcr.io/distroless/base-debian12
33+
FROM gcr.io/distroless/base-debian12@sha256:937c7eaaf6f3f2d38a1f8c4aeff326f0c56e4593ea152e9e8f74d976dde52f56
3434

3535
# Add required MCP server annotation
3636
LABEL io.modelcontextprotocol.server.name="io.github.github/github-mcp-server"

cmd/github-mcp-server/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ var (
6161
}
6262
}
6363

64+
// Parse excluded tools (similar to tools)
65+
var excludeTools []string
66+
if viper.IsSet("exclude_tools") {
67+
if err := viper.UnmarshalKey("exclude_tools", &excludeTools); err != nil {
68+
return fmt.Errorf("failed to unmarshal exclude-tools: %w", err)
69+
}
70+
}
71+
6472
// Parse enabled features (similar to toolsets)
6573
var enabledFeatures []string
6674
if viper.IsSet("features") {
@@ -85,6 +93,7 @@ var (
8593
ContentWindowSize: viper.GetInt("content-window-size"),
8694
LockdownMode: viper.GetBool("lockdown-mode"),
8795
InsidersMode: viper.GetBool("insiders"),
96+
ExcludeTools: excludeTools,
8897
RepoAccessCacheTTL: &ttl,
8998
}
9099
return ghmcp.RunStdioServer(stdioServerConfig)
@@ -126,6 +135,7 @@ func init() {
126135
// Add global flags that will be shared by all commands
127136
rootCmd.PersistentFlags().StringSlice("toolsets", nil, github.GenerateToolsetsHelp())
128137
rootCmd.PersistentFlags().StringSlice("tools", nil, "Comma-separated list of specific tools to enable")
138+
rootCmd.PersistentFlags().StringSlice("exclude-tools", nil, "Comma-separated list of tool names to disable regardless of other settings")
129139
rootCmd.PersistentFlags().StringSlice("features", nil, "Comma-separated list of feature flags to enable")
130140
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
131141
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
@@ -147,6 +157,7 @@ func init() {
147157
// Bind flag to viper
148158
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
149159
_ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools"))
160+
_ = viper.BindPFlag("exclude_tools", rootCmd.PersistentFlags().Lookup("exclude-tools"))
150161
_ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features"))
151162
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
152163
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))

docs/server-configuration.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ We currently support the following ways in which the GitHub MCP Server can be co
99
|---------------|---------------|--------------|
1010
| Toolsets | `X-MCP-Toolsets` header or `/x/{toolset}` URL | `--toolsets` flag or `GITHUB_TOOLSETS` env var |
1111
| Individual Tools | `X-MCP-Tools` header | `--tools` flag or `GITHUB_TOOLS` env var |
12+
| Exclude Tools | `X-MCP-Exclude-Tools` header | `--exclude-tools` flag or `GITHUB_EXCLUDE_TOOLS` env var |
1213
| Read-Only Mode | `X-MCP-Readonly` header or `/readonly` URL | `--read-only` flag or `GITHUB_READ_ONLY` env var |
1314
| Dynamic Mode | Not available | `--dynamic-toolsets` flag or `GITHUB_DYNAMIC_TOOLSETS` env var |
1415
| Lockdown Mode | `X-MCP-Lockdown` header | `--lockdown-mode` flag or `GITHUB_LOCKDOWN_MODE` env var |
@@ -20,10 +21,12 @@ We currently support the following ways in which the GitHub MCP Server can be co
2021

2122
## How Configuration Works
2223

23-
All configuration options are **composable**: you can combine toolsets, individual tools, dynamic discovery, read-only mode and lockdown mode in any way that suits your workflow.
24+
All configuration options are **composable**: you can combine toolsets, individual tools, excluded tools, dynamic discovery, read-only mode and lockdown mode in any way that suits your workflow.
2425

2526
Note: **read-only** mode acts as a strict security filter that takes precedence over any other configuration, by disabling write tools even when explicitly requested.
2627

28+
Note: **excluded tools** takes precedence over toolsets and individual tools — listed tools are always excluded, even if their toolset is enabled or they are explicitly added via `--tools` / `X-MCP-Tools`.
29+
2730
---
2831

2932
## Configuration Examples
@@ -170,6 +173,56 @@ Enable entire toolsets, then add individual tools from toolsets you don't want f
170173

171174
---
172175

176+
### Excluding Specific Tools
177+
178+
**Best for:** Users who want to enable a broad toolset but need to exclude specific tools for security, compliance, or to prevent undesired behavior.
179+
180+
Listed tools are removed regardless of any other configuration — even if their toolset is enabled or they are individually added.
181+
182+
<table>
183+
<tr><th>Remote Server</th><th>Local Server</th></tr>
184+
<tr valign="top">
185+
<td>
186+
187+
```json
188+
{
189+
"type": "http",
190+
"url": "https://api.githubcopilot.com/mcp/",
191+
"headers": {
192+
"X-MCP-Toolsets": "pull_requests",
193+
"X-MCP-Exclude-Tools": "create_pull_request,merge_pull_request"
194+
}
195+
}
196+
```
197+
198+
</td>
199+
<td>
200+
201+
```json
202+
{
203+
"type": "stdio",
204+
"command": "go",
205+
"args": [
206+
"run",
207+
"./cmd/github-mcp-server",
208+
"stdio",
209+
"--toolsets=pull_requests",
210+
"--exclude-tools=create_pull_request,merge_pull_request"
211+
],
212+
"env": {
213+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
214+
}
215+
}
216+
```
217+
218+
</td>
219+
</tr>
220+
</table>
221+
222+
**Result:** All pull request tools except `create_pull_request` and `merge_pull_request` — the user gets read and review tools only.
223+
224+
---
225+
173226
### Read-Only Mode
174227

175228
**Best for:** Security conscious users who want to ensure the server won't allow operations that modify issues, pull requests, repositories etc.

internal/ghmcp/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
135135
WithReadOnly(cfg.ReadOnly).
136136
WithToolsets(github.ResolvedEnabledToolsets(cfg.DynamicToolsets, cfg.EnabledToolsets, cfg.EnabledTools)).
137137
WithTools(github.CleanTools(cfg.EnabledTools)).
138+
WithExcludeTools(cfg.ExcludeTools).
138139
WithServerInstructions().
139140
WithFeatureChecker(featureChecker).
140141
WithInsidersMode(cfg.InsidersMode)
@@ -214,6 +215,11 @@ type StdioServerConfig struct {
214215
// InsidersMode indicates if we should enable experimental features
215216
InsidersMode bool
216217

218+
// ExcludeTools is a list of tool names to disable regardless of other settings.
219+
// These tools will be excluded even if their toolset is enabled or they are
220+
// explicitly listed in EnabledTools.
221+
ExcludeTools []string
222+
217223
// RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
218224
RepoAccessCacheTTL *time.Duration
219225
}
@@ -271,6 +277,7 @@ func RunStdioServer(cfg StdioServerConfig) error {
271277
ContentWindowSize: cfg.ContentWindowSize,
272278
LockdownMode: cfg.LockdownMode,
273279
InsidersMode: cfg.InsidersMode,
280+
ExcludeTools: cfg.ExcludeTools,
274281
Logger: logger,
275282
RepoAccessTTL: cfg.RepoAccessCacheTTL,
276283
TokenScopes: tokenScopes,

pkg/buffer/buffer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const maxLineSize = 10 * 1024 * 1024
3232
// If the response contains more lines than maxJobLogLines, only the most recent lines are kept.
3333
// Lines exceeding maxLineSize are truncated with a marker.
3434
func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines int) (string, int, *http.Response, error) {
35+
if maxJobLogLines <= 0 {
36+
maxJobLogLines = 500
37+
}
3538
if maxJobLogLines > 100000 {
3639
maxJobLogLines = 100000
3740
}

pkg/context/request.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ func IsInsidersMode(ctx context.Context) bool {
8282
return false
8383
}
8484

85+
// excludeToolsCtxKey is a context key for excluded tools
86+
type excludeToolsCtxKey struct{}
87+
88+
// WithExcludeTools adds the excluded tools to the context
89+
func WithExcludeTools(ctx context.Context, tools []string) context.Context {
90+
return context.WithValue(ctx, excludeToolsCtxKey{}, tools)
91+
}
92+
93+
// GetExcludeTools retrieves the excluded tools from the context
94+
func GetExcludeTools(ctx context.Context) []string {
95+
if tools, ok := ctx.Value(excludeToolsCtxKey{}).([]string); ok {
96+
return tools
97+
}
98+
return nil
99+
}
100+
85101
// headerFeaturesCtxKey is a context key for raw header feature flags
86102
type headerFeaturesCtxKey struct{}
87103

pkg/github/actions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,8 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
702702
if err != nil {
703703
return utils.NewToolResultError(err.Error()), nil, nil
704704
}
705-
// Default to 500 lines if not specified
706-
if tailLines == 0 {
705+
// Default to 500 lines if not specified or invalid
706+
if tailLines <= 0 {
707707
tailLines = 500
708708
}
709709

pkg/github/server.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ type MCPServerConfig struct {
6262
// RepoAccessTTL overrides the default TTL for repository access cache entries.
6363
RepoAccessTTL *time.Duration
6464

65+
// ExcludeTools is a list of tool names that should be disabled regardless of
66+
// other configuration. These tools will be excluded even if their toolset is enabled
67+
// or they are explicitly listed in EnabledTools.
68+
ExcludeTools []string
69+
6570
// TokenScopes contains the OAuth scopes available to the token.
6671
// When non-nil, tools requiring scopes not in this list will be hidden.
6772
// This is used for PAT scope filtering where we can't issue scope challenges.
@@ -73,7 +78,7 @@ type MCPServerConfig struct {
7378

7479
type MCPServerOption func(*mcp.ServerOptions)
7580

76-
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inv *inventory.Inventory) (*mcp.Server, error) {
81+
func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependencies, inv *inventory.Inventory, middleware ...mcp.Middleware) (*mcp.Server, error) {
7782
// Create the MCP server
7883
serverOpts := &mcp.ServerOptions{
7984
Instructions: inv.Instructions(),
@@ -98,9 +103,11 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
98103

99104
ghServer := NewServer(cfg.Version, serverOpts)
100105

101-
// Add middlewares
102-
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
106+
// Add middlewares. Order matters - for example, the error context middleware should be applied last so that it runs FIRST (closest to the handler) to ensure all errors are captured,
107+
// and any middleware that needs to read or modify the context should be before it.
108+
ghServer.AddReceivingMiddleware(middleware...)
103109
ghServer.AddReceivingMiddleware(InjectDepsMiddleware(deps))
110+
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
104111

105112
if unrecognized := inv.UnrecognizedToolsets(); len(unrecognized) > 0 {
106113
cfg.Logger.Warn("Warning: unrecognized toolsets ignored", "toolsets", strings.Join(unrecognized, ", "))

pkg/http/handler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
)
2020

2121
type InventoryFactoryFunc func(r *http.Request) (*inventory.Inventory, error)
22+
23+
// GitHubMCPServerFactoryFunc is a function type for creating a new MCP Server instance.
24+
// middleware are applied AFTER the default GitHub MCP Server middlewares (like error context injection)
2225
type GitHubMCPServerFactoryFunc func(r *http.Request, deps github.ToolDependencies, inventory *inventory.Inventory, cfg *github.MCPServerConfig) (*mcp.Server, error)
2326

2427
type Handler struct {
@@ -272,6 +275,10 @@ func InventoryFiltersForRequest(r *http.Request, builder *inventory.Builder) *in
272275
builder = builder.WithTools(github.CleanTools(tools))
273276
}
274277

278+
if excluded := ghcontext.GetExcludeTools(ctx); len(excluded) > 0 {
279+
builder = builder.WithExcludeTools(excluded)
280+
}
281+
275282
return builder
276283
}
277284

pkg/http/handler_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ func TestInventoryFiltersForRequest(t *testing.T) {
104104
},
105105
expectedTools: []string{"get_file_contents", "create_repository", "list_issues"},
106106
},
107+
{
108+
name: "excluded tools removes specific tools",
109+
contextSetup: func(ctx context.Context) context.Context {
110+
return ghcontext.WithExcludeTools(ctx, []string{"create_repository", "issue_write"})
111+
},
112+
expectedTools: []string{"get_file_contents", "list_issues"},
113+
},
114+
{
115+
name: "excluded tools overrides explicit tools",
116+
contextSetup: func(ctx context.Context) context.Context {
117+
ctx = ghcontext.WithTools(ctx, []string{"list_issues", "create_repository"})
118+
ctx = ghcontext.WithExcludeTools(ctx, []string{"create_repository"})
119+
return ctx
120+
},
121+
expectedTools: []string{"list_issues"},
122+
},
123+
{
124+
name: "excluded tools combines with readonly",
125+
contextSetup: func(ctx context.Context) context.Context {
126+
ctx = ghcontext.WithReadonly(ctx, true)
127+
ctx = ghcontext.WithExcludeTools(ctx, []string{"list_issues"})
128+
return ctx
129+
},
130+
expectedTools: []string{"get_file_contents"},
131+
},
107132
}
108133

109134
for _, tt := range tests {
@@ -267,6 +292,40 @@ func TestHTTPHandlerRoutes(t *testing.T) {
267292
},
268293
expectedTools: []string{"get_file_contents", "create_repository", "list_issues", "create_issue", "list_pull_requests", "create_pull_request", "hidden_by_holdback"},
269294
},
295+
{
296+
name: "X-MCP-Exclude-Tools header removes specific tools",
297+
path: "/",
298+
headers: map[string]string{
299+
headers.MCPExcludeToolsHeader: "create_issue,create_pull_request",
300+
},
301+
expectedTools: []string{"get_file_contents", "create_repository", "list_issues", "list_pull_requests", "hidden_by_holdback"},
302+
},
303+
{
304+
name: "X-MCP-Exclude-Tools with toolset header",
305+
path: "/",
306+
headers: map[string]string{
307+
headers.MCPToolsetsHeader: "issues",
308+
headers.MCPExcludeToolsHeader: "create_issue",
309+
},
310+
expectedTools: []string{"list_issues"},
311+
},
312+
{
313+
name: "X-MCP-Exclude-Tools overrides X-MCP-Tools",
314+
path: "/",
315+
headers: map[string]string{
316+
headers.MCPToolsHeader: "list_issues,create_issue",
317+
headers.MCPExcludeToolsHeader: "create_issue",
318+
},
319+
expectedTools: []string{"list_issues"},
320+
},
321+
{
322+
name: "X-MCP-Exclude-Tools with readonly path",
323+
path: "/readonly",
324+
headers: map[string]string{
325+
headers.MCPExcludeToolsHeader: "list_issues",
326+
},
327+
expectedTools: []string{"get_file_contents", "list_pull_requests", "hidden_by_holdback"},
328+
},
270329
}
271330

272331
for _, tt := range tests {

0 commit comments

Comments
 (0)