Skip to content

Commit 53a14c8

Browse files
committed
Revert "Simplify MCP App UIs for basic branch"
This reverts commit 24174b9.
1 parent 24174b9 commit 53a14c8

File tree

6 files changed

+1592
-36
lines changed

6 files changed

+1592
-36
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "Get UI data"
5+
},
6+
"description": "Fetch UI data for MCP Apps (labels, assignees, milestones, issue types, branches).",
7+
"inputSchema": {
8+
"properties": {
9+
"method": {
10+
"description": "The type of data to fetch",
11+
"enum": [
12+
"labels",
13+
"assignees",
14+
"milestones",
15+
"issue_types",
16+
"branches"
17+
],
18+
"type": "string"
19+
},
20+
"owner": {
21+
"description": "Repository owner (required for all methods)",
22+
"type": "string"
23+
},
24+
"repo": {
25+
"description": "Repository name (required for labels, assignees, milestones, branches)",
26+
"type": "string"
27+
}
28+
},
29+
"required": [
30+
"method",
31+
"owner"
32+
],
33+
"type": "object"
34+
},
35+
"name": "ui_get"
36+
}

pkg/github/tools.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
295295
GetLabelForLabelsToolset(t),
296296
ListLabels(t),
297297
LabelWrite(t),
298+
299+
// UI tools (insiders only)
300+
UIGet(t),
298301
}
299302
}
300303

pkg/github/ui_tools.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
10+
ghErrors "github.com/github/github-mcp-server/pkg/errors"
11+
"github.com/github/github-mcp-server/pkg/inventory"
12+
"github.com/github/github-mcp-server/pkg/scopes"
13+
"github.com/github/github-mcp-server/pkg/translations"
14+
"github.com/github/github-mcp-server/pkg/utils"
15+
"github.com/google/go-github/v79/github"
16+
"github.com/google/jsonschema-go/jsonschema"
17+
"github.com/modelcontextprotocol/go-sdk/mcp"
18+
"github.com/shurcooL/githubv4"
19+
)
20+
21+
// UIGet creates a tool to fetch UI data for MCP Apps.
22+
func UIGet(t translations.TranslationHelperFunc) inventory.ServerTool {
23+
st := NewTool(
24+
ToolsetMetadataContext, // Use context toolset so it's always available
25+
mcp.Tool{
26+
Name: "ui_get",
27+
Description: t("TOOL_UI_GET_DESCRIPTION", "Fetch UI data for MCP Apps (labels, assignees, milestones, issue types, branches)."),
28+
Annotations: &mcp.ToolAnnotations{
29+
Title: t("TOOL_UI_GET_USER_TITLE", "Get UI data"),
30+
ReadOnlyHint: true,
31+
},
32+
InputSchema: &jsonschema.Schema{
33+
Type: "object",
34+
Properties: map[string]*jsonschema.Schema{
35+
"method": {
36+
Type: "string",
37+
Enum: []any{"labels", "assignees", "milestones", "issue_types", "branches"},
38+
Description: "The type of data to fetch",
39+
},
40+
"owner": {
41+
Type: "string",
42+
Description: "Repository owner (required for all methods)",
43+
},
44+
"repo": {
45+
Type: "string",
46+
Description: "Repository name (required for labels, assignees, milestones, branches)",
47+
},
48+
},
49+
Required: []string{"method", "owner"},
50+
},
51+
},
52+
[]scopes.Scope{scopes.Repo, scopes.ReadOrg},
53+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
54+
method, err := RequiredParam[string](args, "method")
55+
if err != nil {
56+
return utils.NewToolResultError(err.Error()), nil, nil
57+
}
58+
59+
owner, err := RequiredParam[string](args, "owner")
60+
if err != nil {
61+
return utils.NewToolResultError(err.Error()), nil, nil
62+
}
63+
64+
switch method {
65+
case "labels":
66+
return uiGetLabels(ctx, deps, args, owner)
67+
case "assignees":
68+
return uiGetAssignees(ctx, deps, args, owner)
69+
case "milestones":
70+
return uiGetMilestones(ctx, deps, args, owner)
71+
case "issue_types":
72+
return uiGetIssueTypes(ctx, deps, owner)
73+
case "branches":
74+
return uiGetBranches(ctx, deps, args, owner)
75+
default:
76+
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
77+
}
78+
})
79+
st.InsidersOnly = true
80+
return st
81+
}
82+
83+
func uiGetLabels(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
84+
repo, err := RequiredParam[string](args, "repo")
85+
if err != nil {
86+
return utils.NewToolResultError(err.Error()), nil, nil
87+
}
88+
89+
client, err := deps.GetGQLClient(ctx)
90+
if err != nil {
91+
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
92+
}
93+
94+
var query struct {
95+
Repository struct {
96+
Labels struct {
97+
Nodes []struct {
98+
ID githubv4.ID
99+
Name githubv4.String
100+
Color githubv4.String
101+
Description githubv4.String
102+
}
103+
TotalCount githubv4.Int
104+
} `graphql:"labels(first: 100)"`
105+
} `graphql:"repository(owner: $owner, name: $repo)"`
106+
}
107+
108+
vars := map[string]any{
109+
"owner": githubv4.String(owner),
110+
"repo": githubv4.String(repo),
111+
}
112+
113+
if err := client.Query(ctx, &query, vars); err != nil {
114+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil
115+
}
116+
117+
labels := make([]map[string]any, len(query.Repository.Labels.Nodes))
118+
for i, labelNode := range query.Repository.Labels.Nodes {
119+
labels[i] = map[string]any{
120+
"id": fmt.Sprintf("%v", labelNode.ID),
121+
"name": string(labelNode.Name),
122+
"color": string(labelNode.Color),
123+
"description": string(labelNode.Description),
124+
}
125+
}
126+
127+
response := map[string]any{
128+
"labels": labels,
129+
"totalCount": int(query.Repository.Labels.TotalCount),
130+
}
131+
132+
out, err := json.Marshal(response)
133+
if err != nil {
134+
return nil, nil, fmt.Errorf("failed to marshal labels: %w", err)
135+
}
136+
137+
return utils.NewToolResultText(string(out)), nil, nil
138+
}
139+
140+
func uiGetAssignees(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
141+
repo, err := RequiredParam[string](args, "repo")
142+
if err != nil {
143+
return utils.NewToolResultError(err.Error()), nil, nil
144+
}
145+
146+
client, err := deps.GetClient(ctx)
147+
if err != nil {
148+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
149+
}
150+
151+
opts := &github.ListOptions{PerPage: 100}
152+
var allAssignees []*github.User
153+
154+
for {
155+
assignees, resp, err := client.Issues.ListAssignees(ctx, owner, repo, opts)
156+
if err != nil {
157+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list assignees", resp, err), nil, nil
158+
}
159+
allAssignees = append(allAssignees, assignees...)
160+
if resp.NextPage == 0 {
161+
break
162+
}
163+
opts.Page = resp.NextPage
164+
}
165+
166+
result := make([]map[string]string, len(allAssignees))
167+
for i, u := range allAssignees {
168+
result[i] = map[string]string{
169+
"login": u.GetLogin(),
170+
"avatar_url": u.GetAvatarURL(),
171+
}
172+
}
173+
174+
out, err := json.Marshal(map[string]any{
175+
"assignees": result,
176+
"totalCount": len(result),
177+
})
178+
if err != nil {
179+
return utils.NewToolResultErrorFromErr("failed to marshal assignees", err), nil, nil
180+
}
181+
182+
return utils.NewToolResultText(string(out)), nil, nil
183+
}
184+
185+
func uiGetMilestones(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
186+
repo, err := RequiredParam[string](args, "repo")
187+
if err != nil {
188+
return utils.NewToolResultError(err.Error()), nil, nil
189+
}
190+
191+
client, err := deps.GetClient(ctx)
192+
if err != nil {
193+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
194+
}
195+
196+
opts := &github.MilestoneListOptions{
197+
State: "open",
198+
ListOptions: github.ListOptions{PerPage: 100},
199+
}
200+
201+
var allMilestones []*github.Milestone
202+
for {
203+
milestones, resp, err := client.Issues.ListMilestones(ctx, owner, repo, opts)
204+
if err != nil {
205+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list milestones", resp, err), nil, nil
206+
}
207+
allMilestones = append(allMilestones, milestones...)
208+
if resp.NextPage == 0 {
209+
break
210+
}
211+
opts.Page = resp.NextPage
212+
}
213+
214+
result := make([]map[string]any, len(allMilestones))
215+
for i, m := range allMilestones {
216+
result[i] = map[string]any{
217+
"number": m.GetNumber(),
218+
"title": m.GetTitle(),
219+
"description": m.GetDescription(),
220+
"state": m.GetState(),
221+
"open_issues": m.GetOpenIssues(),
222+
"due_on": m.GetDueOn().Format("2006-01-02"),
223+
}
224+
}
225+
226+
out, err := json.Marshal(map[string]any{
227+
"milestones": result,
228+
"totalCount": len(result),
229+
})
230+
if err != nil {
231+
return utils.NewToolResultErrorFromErr("failed to marshal milestones", err), nil, nil
232+
}
233+
234+
return utils.NewToolResultText(string(out)), nil, nil
235+
}
236+
237+
func uiGetIssueTypes(ctx context.Context, deps ToolDependencies, owner string) (*mcp.CallToolResult, any, error) {
238+
client, err := deps.GetClient(ctx)
239+
if err != nil {
240+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
241+
}
242+
243+
issueTypes, resp, err := client.Organizations.ListIssueTypes(ctx, owner)
244+
if err != nil {
245+
return utils.NewToolResultErrorFromErr("failed to list issue types", err), nil, nil
246+
}
247+
defer func() { _ = resp.Body.Close() }()
248+
249+
if resp.StatusCode != http.StatusOK {
250+
body, err := io.ReadAll(resp.Body)
251+
if err != nil {
252+
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
253+
}
254+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil
255+
}
256+
257+
r, err := json.Marshal(issueTypes)
258+
if err != nil {
259+
return utils.NewToolResultErrorFromErr("failed to marshal issue types", err), nil, nil
260+
}
261+
262+
return utils.NewToolResultText(string(r)), nil, nil
263+
}
264+
265+
func uiGetBranches(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
266+
repo, err := RequiredParam[string](args, "repo")
267+
if err != nil {
268+
return utils.NewToolResultError(err.Error()), nil, nil
269+
}
270+
271+
client, err := deps.GetClient(ctx)
272+
if err != nil {
273+
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
274+
}
275+
276+
opts := &github.BranchListOptions{
277+
ListOptions: github.ListOptions{PerPage: 100},
278+
}
279+
280+
branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
281+
if err != nil {
282+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list branches", resp, err), nil, nil
283+
}
284+
defer func() { _ = resp.Body.Close() }()
285+
286+
if resp.StatusCode != http.StatusOK {
287+
body, err := io.ReadAll(resp.Body)
288+
if err != nil {
289+
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
290+
}
291+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list branches", resp, body), nil, nil
292+
}
293+
294+
minimalBranches := make([]MinimalBranch, 0, len(branches))
295+
for _, branch := range branches {
296+
minimalBranches = append(minimalBranches, convertToMinimalBranch(branch))
297+
}
298+
299+
r, err := json.Marshal(map[string]any{
300+
"branches": minimalBranches,
301+
"totalCount": len(minimalBranches),
302+
})
303+
if err != nil {
304+
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
305+
}
306+
307+
return utils.NewToolResultText(string(r)), nil, nil
308+
}

0 commit comments

Comments
 (0)