Skip to content

Commit 5b81c2e

Browse files
committed
add create pull request functionality with UI support and insiders
1 parent 1868857 commit 5b81c2e

14 files changed

Lines changed: 1108 additions & 73 deletions

File tree

internal/ghmcp/server.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,51 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
115115
// Create feature checker
116116
featureChecker := createFeatureChecker(cfg.EnabledFeatures)
117117

118+
// Build and register the tool/resource/prompt inventory
119+
inventoryBuilder := github.NewInventory(cfg.Translator).
120+
WithDeprecatedAliases(github.DeprecatedToolAliases).
121+
WithReadOnly(cfg.ReadOnly).
122+
WithToolsets(enabledToolsets).
123+
WithTools(cfg.EnabledTools).
124+
WithFeatureChecker(featureChecker).
125+
WithInsidersMode(cfg.InsidersMode).
126+
WithServerInstructions()
127+
128+
// Apply token scope filtering if scopes are known (for PAT filtering)
129+
if cfg.TokenScopes != nil {
130+
inventoryBuilder = inventoryBuilder.WithFilter(github.CreateToolScopeFilter(cfg.TokenScopes))
131+
}
132+
133+
inventory, err := inventoryBuilder.Build()
134+
if err != nil {
135+
return nil, fmt.Errorf("failed to build inventory: %w", err)
136+
}
137+
138+
// Create the MCP server
139+
serverOpts := &mcp.ServerOptions{
140+
Instructions: inventory.Instructions(),
141+
Logger: cfg.Logger,
142+
CompletionHandler: github.CompletionsHandler(func(_ context.Context) (*gogithub.Client, error) {
143+
return clients.rest, nil
144+
}),
145+
}
146+
147+
// In dynamic mode, explicitly advertise capabilities since tools/resources/prompts
148+
// may be enabled at runtime even if none are registered initially.
149+
if cfg.DynamicToolsets {
150+
serverOpts.Capabilities = &mcp.ServerCapabilities{
151+
Tools: &mcp.ToolCapabilities{},
152+
Resources: &mcp.ResourceCapabilities{},
153+
Prompts: &mcp.PromptCapabilities{},
154+
}
155+
}
156+
157+
ghServer := github.NewServer(cfg.Version, serverOpts)
158+
159+
// Add middlewares
160+
ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
161+
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.rest, clients.gqlHTTP))
162+
118163
// Create dependencies for tool handlers
119164
deps := github.NewBaseDeps(
120165
clients.rest,
@@ -143,9 +188,21 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
143188
inventoryBuilder = inventoryBuilder.WithFilter(github.CreateToolScopeFilter(cfg.TokenScopes))
144189
}
145190

146-
inventory, err := inventoryBuilder.Build()
147-
if err != nil {
148-
return nil, fmt.Errorf("failed to build inventory: %w", err)
191+
// Register GitHub tools/resources/prompts from the inventory.
192+
// In dynamic mode with no explicit toolsets, this is a no-op since enabledToolsets
193+
// is empty - users enable toolsets at runtime via the dynamic tools below (but can
194+
// enable toolsets or tools explicitly that do need registration).
195+
inventory.RegisterAll(context.Background(), ghServer, deps)
196+
197+
// Register MCP App UI resources (static resources for tool UI) - insiders only
198+
if cfg.InsidersMode {
199+
github.RegisterUIResources(ghServer)
200+
}
201+
202+
// Register dynamic toolset management tools (enable/disable) - these are separate
203+
// meta-tools that control the inventory, not part of the inventory itself
204+
if cfg.DynamicToolsets {
205+
registerDynamicTools(ghServer, inventory, deps, cfg.Translator)
149206
}
150207

151208
ghServer, err := github.NewMCPServer(ctx, &cfg, deps, inventory)

pkg/github/__toolsnaps__/create_pull_request.snap

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
2+
"_meta": {
3+
"ui": {
4+
"resourceUri": "ui://github-mcp-server/pr-write",
5+
"visibility": [
6+
"model",
7+
"app"
8+
]
9+
}
10+
},
211
"annotations": {
312
"title": "Open new pull request"
413
},
5-
"description": "Create a new pull request in a GitHub repository.",
14+
"description": "Create a new pull request in a GitHub repository.\n\nWhen show_ui is true, an interactive form is displayed for the user to fill in PR details. Use show_ui when:\n- Creating a new PR and you want user input on the details\n- The user hasn't specified all required fields (title, head, base, etc.)\n- Interactive feedback would be valuable (branch selection, reviewers, labels)\n\nWhen show_ui is false or omitted, the PR is created directly with the provided parameters.",
615
"inputSchema": {
716
"properties": {
817
"base": {
@@ -33,17 +42,18 @@
3342
"description": "Repository name",
3443
"type": "string"
3544
},
45+
"show_ui": {
46+
"description": "If true, show an interactive form for the user to fill in PR details. If false or omitted, create the PR directly with the provided parameters.",
47+
"type": "boolean"
48+
},
3649
"title": {
3750
"description": "PR title",
3851
"type": "string"
3952
}
4053
},
4154
"required": [
4255
"owner",
43-
"repo",
44-
"title",
45-
"head",
46-
"base"
56+
"repo"
4757
],
4858
"type": "object"
4959
},

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"type": "string"
6262
},
6363
"show_ui": {
64-
"description": "If true, show an interactive form for the user to fill in issue details. If false or omitted, create/update the issue directly with the provided parameters. Use show_ui when you want user input or when not all fields are specified.",
64+
"description": "If true, show an interactive form for the user to fill in issue details. If false or omitted, create/update the issue directly with the provided parameters.",
6565
"type": "boolean"
6666
},
6767
"state": {

pkg/github/context_tools.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
5454
// Use json.RawMessage to ensure "properties" is included even when empty.
5555
// OpenAI strict mode requires the properties field to be present.
5656
InputSchema: json.RawMessage(`{"type":"object","properties":{}}`),
57+
Meta: mcp.Meta{
58+
"ui": map[string]any{
59+
"resourceUri": GetMeUIResourceURI,
60+
},
61+
},
5762
},
5863
nil,
5964
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {

pkg/github/issues.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,6 @@ When show_ui is false or omitted, the issue is created/updated directly with the
11901190
Title: t("TOOL_ISSUE_WRITE_USER_TITLE", "Create or update issue."),
11911191
ReadOnlyHint: false,
11921192
},
1193-
// MCP Apps UI metadata - links this tool to its UI resource
11941193
Meta: mcp.Meta{
11951194
"ui": map[string]any{
11961195
"resourceUri": IssueWriteUIResourceURI,
@@ -1202,7 +1201,7 @@ When show_ui is false or omitted, the issue is created/updated directly with the
12021201
Properties: map[string]*jsonschema.Schema{
12031202
"show_ui": {
12041203
Type: "boolean",
1205-
Description: "If true, show an interactive form for the user to fill in issue details. If false or omitted, create/update the issue directly with the provided parameters. Use show_ui when you want user input or when not all fields are specified.",
1204+
Description: "If true, show an interactive form for the user to fill in issue details. If false or omitted, create/update the issue directly with the provided parameters.",
12061205
},
12071206
"method": {
12081207
Type: "string",
@@ -1295,9 +1294,9 @@ Options are:
12951294
return utils.NewToolResultError(err.Error()), nil, nil
12961295
}
12971296

1298-
// If show_ui is true, return a message indicating the UI should be shown
1297+
// If show_ui is true and insiders mode is enabled, return a message indicating the UI should be shown
12991298
// The host will detect the UI metadata and display the form
1300-
if showUI {
1299+
if showUI && deps.GetFlags().InsidersMode {
13011300
if method == "update" {
13021301
issueNumber, numErr := RequiredInt(args, "issue_number")
13031302
if numErr != nil {

pkg/github/pullrequests.go

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -485,57 +485,75 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, deps Tool
485485
return utils.NewToolResultText(string(r)), nil
486486
}
487487

488+
// PullRequestWriteUIResourceURI is the URI for the create_pull_request tool's MCP App UI resource.
489+
const PullRequestWriteUIResourceURI = "ui://github-mcp-server/pr-write"
490+
488491
// CreatePullRequest creates a tool to create a new pull request.
489492
func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool {
490-
schema := &jsonschema.Schema{
491-
Type: "object",
492-
Properties: map[string]*jsonschema.Schema{
493-
"owner": {
494-
Type: "string",
495-
Description: "Repository owner",
496-
},
497-
"repo": {
498-
Type: "string",
499-
Description: "Repository name",
500-
},
501-
"title": {
502-
Type: "string",
503-
Description: "PR title",
504-
},
505-
"body": {
506-
Type: "string",
507-
Description: "PR description",
508-
},
509-
"head": {
510-
Type: "string",
511-
Description: "Branch containing changes",
512-
},
513-
"base": {
514-
Type: "string",
515-
Description: "Branch to merge into",
516-
},
517-
"draft": {
518-
Type: "boolean",
519-
Description: "Create as draft PR",
520-
},
521-
"maintainer_can_modify": {
522-
Type: "boolean",
523-
Description: "Allow maintainer edits",
524-
},
525-
},
526-
Required: []string{"owner", "repo", "title", "head", "base"},
527-
}
528-
529493
return NewTool(
530494
ToolsetMetadataPullRequests,
531495
mcp.Tool{
532-
Name: "create_pull_request",
533-
Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."),
496+
Name: "create_pull_request",
497+
Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", `Create a new pull request in a GitHub repository.
498+
499+
When show_ui is true, an interactive form is displayed for the user to fill in PR details. Use show_ui when:
500+
- Creating a new PR and you want user input on the details
501+
- The user hasn't specified all required fields (title, head, base, etc.)
502+
- Interactive feedback would be valuable (branch selection, reviewers, labels)
503+
504+
When show_ui is false or omitted, the PR is created directly with the provided parameters.`),
534505
Annotations: &mcp.ToolAnnotations{
535506
Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"),
536507
ReadOnlyHint: false,
537508
},
538-
InputSchema: schema,
509+
Meta: mcp.Meta{
510+
"ui": map[string]any{
511+
"resourceUri": PullRequestWriteUIResourceURI,
512+
"visibility": []string{"model", "app"},
513+
},
514+
},
515+
InputSchema: &jsonschema.Schema{
516+
Type: "object",
517+
Properties: map[string]*jsonschema.Schema{
518+
"show_ui": {
519+
Type: "boolean",
520+
Description: "If true, show an interactive form for the user to fill in PR details. If false or omitted, create the PR directly with the provided parameters.",
521+
},
522+
"owner": {
523+
Type: "string",
524+
Description: "Repository owner",
525+
},
526+
"repo": {
527+
Type: "string",
528+
Description: "Repository name",
529+
},
530+
"title": {
531+
Type: "string",
532+
Description: "PR title",
533+
},
534+
"body": {
535+
Type: "string",
536+
Description: "PR description",
537+
},
538+
"head": {
539+
Type: "string",
540+
Description: "Branch containing changes",
541+
},
542+
"base": {
543+
Type: "string",
544+
Description: "Branch to merge into",
545+
},
546+
"draft": {
547+
Type: "boolean",
548+
Description: "Create as draft PR",
549+
},
550+
"maintainer_can_modify": {
551+
Type: "boolean",
552+
Description: "Allow maintainer edits",
553+
},
554+
},
555+
Required: []string{"owner", "repo"},
556+
},
539557
},
540558
[]scopes.Scope{scopes.Repo},
541559
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
@@ -547,6 +565,19 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
547565
if err != nil {
548566
return utils.NewToolResultError(err.Error()), nil, nil
549567
}
568+
569+
// Check if UI mode is requested
570+
showUI, err := OptionalParam[bool](args, "show_ui")
571+
if err != nil {
572+
return utils.NewToolResultError(err.Error()), nil, nil
573+
}
574+
575+
// If show_ui is true and insiders mode is enabled, return a message indicating the UI should be shown
576+
if showUI && deps.GetFlags().InsidersMode {
577+
return utils.NewToolResultText(fmt.Sprintf("Ready to create a pull request in %s/%s. The interactive form will be displayed.", owner, repo)), nil, nil
578+
}
579+
580+
// When not using UI, title/head/base are required
550581
title, err := RequiredParam[string](args, "title")
551582
if err != nil {
552583
return utils.NewToolResultError(err.Error()), nil, nil

pkg/github/pullrequests_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,7 @@ func Test_CreatePullRequest(t *testing.T) {
20312031
assert.Equal(t, "create_pull_request", tool.Name)
20322032
assert.NotEmpty(t, tool.Description)
20332033
schema := tool.InputSchema.(*jsonschema.Schema)
2034+
assert.Contains(t, schema.Properties, "show_ui")
20342035
assert.Contains(t, schema.Properties, "owner")
20352036
assert.Contains(t, schema.Properties, "repo")
20362037
assert.Contains(t, schema.Properties, "title")
@@ -2039,7 +2040,7 @@ func Test_CreatePullRequest(t *testing.T) {
20392040
assert.Contains(t, schema.Properties, "base")
20402041
assert.Contains(t, schema.Properties, "draft")
20412042
assert.Contains(t, schema.Properties, "maintainer_can_modify")
2042-
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "title", "head", "base"})
2043+
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"})
20432044

20442045
// Setup mock PR for success case
20452046
mockPR := &github.PullRequest{

pkg/github/ui_resources.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,26 @@ func RegisterUIResources(s *mcp.Server) {
6464
}, nil
6565
},
6666
)
67+
68+
// Register the create_pull_request UI resource
69+
s.AddResource(
70+
&mcp.Resource{
71+
URI: PullRequestWriteUIResourceURI,
72+
Name: "pr_write_ui",
73+
Description: "MCP App UI for creating GitHub pull requests",
74+
MIMEType: "text/html",
75+
},
76+
func(_ context.Context, _ *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
77+
html := MustGetUIAsset("pr-write.html")
78+
return &mcp.ReadResourceResult{
79+
Contents: []*mcp.ResourceContents{
80+
{
81+
URI: PullRequestWriteUIResourceURI,
82+
MIMEType: "text/html",
83+
Text: html,
84+
},
85+
},
86+
}, nil
87+
},
88+
)
6789
}

0 commit comments

Comments
 (0)