Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
// remote/HTTP server also serves them, fixing the "-32002 Resource not
// found" error clients hit after the tool returns a ui:// URI.
if UIAssetsAvailable() {
RegisterUIResources(ghServer)
RegisterUIResources(ghServer, cfg.ReadOnly)
}

return ghServer, nil
Expand Down
6 changes: 5 additions & 1 deletion pkg/github/ui_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
//
// Resource metadata follows the stable 2026-01-26 MCP Apps spec:
// https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
func RegisterUIResources(s *mcp.Server) {
func RegisterUIResources(s *mcp.Server, readOnly bool) {
// Register the get_me UI resource
s.AddResource(
&mcp.Resource{
Expand Down Expand Up @@ -46,6 +46,10 @@ func RegisterUIResources(s *mcp.Server) {
},
)

if readOnly {
return
}

// Register the issue_write UI resource
s.AddResource(
&mcp.Resource{
Expand Down
46 changes: 45 additions & 1 deletion pkg/github/ui_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package github

import (
"context"
"slices"
"testing"

"github.com/github/github-mcp-server/pkg/inventory"
Expand All @@ -26,7 +27,7 @@ func TestRegisterUIResources_ReadableViaClient(t *testing.T) {
}

srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil)
RegisterUIResources(srv)
RegisterUIResources(srv, false)

// Connect an in-memory client/server pair and read each advertised URI.
st, ct := mcp.NewInMemoryTransports()
Expand Down Expand Up @@ -113,6 +114,49 @@ func TestNewMCPServer_RegistersUIResources(t *testing.T) {
assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType)
}

func TestRegisterUIResources_ReadOnlySkipsWriteResources(t *testing.T) {
t.Parallel()

srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil)
RegisterUIResources(srv, true)

st, ct := mcp.NewInMemoryTransports()

type clientResult struct {
res *mcp.ListResourcesResult
err error
}
clientCh := make(chan clientResult, 1)
go func() {
client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil)
cs, err := client.Connect(context.Background(), ct, nil)
if err != nil {
clientCh <- clientResult{err: err}
return
}
defer func() { _ = cs.Close() }()

res, err := cs.ListResources(context.Background(), nil)
clientCh <- clientResult{res: res, err: err}
}()

ss, err := srv.Connect(context.Background(), st, nil)
require.NoError(t, err)
t.Cleanup(func() { _ = ss.Close() })

got := <-clientCh
require.NoError(t, got.err)
require.NotNil(t, got.res)

names := make([]string, 0, len(got.res.Resources))
for _, res := range got.res.Resources {
names = append(names, res.Name)
}
slices.Sort(names)

assert.Equal(t, []string{"get_me_ui"}, names)
}

// mustEmptyInventory builds an empty inventory for tests that only care about
// resources/prompts registered outside the inventory (such as the UI resources).
func mustEmptyInventory(t *testing.T) *inventory.Inventory {
Expand Down