Skip to content

Commit c7e8f0e

Browse files
committed
fix: hide write UI resources in read-only mode
1 parent 1add5fe commit c7e8f0e

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

internal/ghmcp/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
179179
// requiring a UI build (graceful degradation).
180180
mcpAppsEnabled, _ := featureChecker(context.Background(), github.MCPAppsFeatureFlag)
181181
if mcpAppsEnabled && github.UIAssetsAvailable() {
182-
github.RegisterUIResources(ghServer)
182+
github.RegisterUIResources(ghServer, cfg.ReadOnly)
183183
}
184184

185185
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.restUATransp, clients.gqlHTTP))

pkg/github/ui_resources.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
// These are static resources (not templates) that serve HTML content for
1111
// MCP App-enabled tools. The HTML is built from React/Primer components
1212
// in the ui/ directory using `script/build-ui`.
13-
func RegisterUIResources(s *mcp.Server) {
13+
func RegisterUIResources(s *mcp.Server, readOnly bool) {
1414
// Register the get_me UI resource
1515
s.AddResource(
1616
&mcp.Resource{
@@ -43,6 +43,10 @@ func RegisterUIResources(s *mcp.Server) {
4343
},
4444
)
4545

46+
if readOnly {
47+
return
48+
}
49+
4650
// Register the issue_write UI resource
4751
s.AddResource(
4852
&mcp.Resource{

pkg/github/ui_resources_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"slices"
6+
"testing"
7+
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func listUIResourceNames(t *testing.T, readOnly bool) []string {
13+
t.Helper()
14+
15+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
16+
RegisterUIResources(srv, readOnly)
17+
18+
st, ct := mcp.NewInMemoryTransports()
19+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil)
20+
21+
type clientResult struct {
22+
res *mcp.ListResourcesResult
23+
err error
24+
}
25+
clientResultCh := make(chan clientResult, 1)
26+
go func() {
27+
cs, err := client.Connect(context.Background(), ct, nil)
28+
if err != nil {
29+
clientResultCh <- clientResult{err: err}
30+
return
31+
}
32+
defer func() { _ = cs.Close() }()
33+
34+
res, err := cs.ListResources(context.Background(), nil)
35+
clientResultCh <- clientResult{res: res, err: err}
36+
}()
37+
38+
ss, err := srv.Connect(context.Background(), st, nil)
39+
require.NoError(t, err)
40+
t.Cleanup(func() { _ = ss.Close() })
41+
42+
got := <-clientResultCh
43+
require.NoError(t, got.err)
44+
require.NotNil(t, got.res)
45+
46+
names := make([]string, 0, len(got.res.Resources))
47+
for _, res := range got.res.Resources {
48+
names = append(names, res.Name)
49+
}
50+
slices.Sort(names)
51+
return names
52+
}
53+
54+
func TestRegisterUIResources(t *testing.T) {
55+
t.Parallel()
56+
57+
t.Run("registers all UI resources by default", func(t *testing.T) {
58+
t.Parallel()
59+
60+
names := listUIResourceNames(t, false)
61+
require.Equal(t, []string{"get_me_ui", "issue_write_ui", "pr_write_ui"}, names)
62+
})
63+
64+
t.Run("skips write UI resources in read-only mode", func(t *testing.T) {
65+
t.Parallel()
66+
67+
names := listUIResourceNames(t, true)
68+
require.Equal(t, []string{"get_me_ui"}, names)
69+
})
70+
}

0 commit comments

Comments
 (0)