Skip to content

Commit 6564023

Browse files
Merge remote-tracking branch 'origin/main' into rosstarrant/add-csv-output-structure
# Conflicts: # README.md
2 parents bd8c160 + f39f758 commit 6564023

15 files changed

Lines changed: 1249 additions & 241 deletions

README.md

Lines changed: 12 additions & 173 deletions
Large diffs are not rendered by default.

pkg/github/__toolsnaps__/get_label.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"annotations": {
33
"readOnlyHint": true,
4-
"title": "Get a specific label from a repository."
4+
"title": "Get a specific label from a repository"
55
},
66
"description": "Get a specific label from a repository.",
77
"inputSchema": {

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010
},
1111
"annotations": {
12-
"title": "Create or update issue."
12+
"title": "Create or update issue"
1313
},
1414
"description": "Create a new or update an existing issue in a GitHub repository.",
1515
"inputSchema": {

pkg/github/__toolsnaps__/label_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"annotations": {
3-
"title": "Write operations on repository labels."
3+
"title": "Write operations on repository labels"
44
},
55
"description": "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.",
66
"inputSchema": {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "List issue fields"
5+
},
6+
"description": "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select), and for single_select fields the list of valid option names. When repo is omitted, returns org-level fields directly.",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "The account owner of the repository or organization. The name is not case sensitive.",
11+
"type": "string"
12+
},
13+
"repo": {
14+
"description": "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"owner"
20+
],
21+
"type": "object"
22+
},
23+
"name": "list_issue_fields"
24+
}

pkg/github/__toolsnaps__/list_issues.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
],
1919
"type": "string"
2020
},
21+
"field_filters": {
22+
"description": "Filter by custom issue field values. Each entry takes a field_name and a value; the server looks up the field and coerces the value to its type (single-select option name, text, number, or YYYY-MM-DD date).",
23+
"items": {
24+
"properties": {
25+
"field_name": {
26+
"description": "Name of the custom field (e.g. \"Priority\"). Case-insensitive.",
27+
"type": "string"
28+
},
29+
"value": {
30+
"description": "Value to filter on. For single-select fields, the option name (e.g. \"P1\"). For dates, YYYY-MM-DD. For numbers, the numeric value as a string. For text, the text value.",
31+
"type": "string"
32+
}
33+
},
34+
"required": [
35+
"field_name",
36+
"value"
37+
],
38+
"type": "object"
39+
},
40+
"type": "array"
41+
},
2142
"labels": {
2243
"description": "Filter by labels",
2344
"items": {

pkg/github/__toolsnaps__/list_label.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"annotations": {
33
"readOnlyHint": true,
4-
"title": "List labels from a repository."
4+
"title": "List labels from a repository"
55
},
66
"description": "List labels from a repository",
77
"inputSchema": {

pkg/github/__toolsnaps__/pull_request_review_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"annotations": {
3-
"title": "Write operations (create, submit, delete) on pull request reviews."
3+
"title": "Write operations (create, submit, delete) on pull request reviews"
44
},
55
"description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n- resolve_thread: Resolve a review thread. Requires only \"threadId\" parameter with the thread's node ID (e.g., PRRT_kwDOxxx). The owner, repo, and pullNumber parameters are not used for this method. Resolving an already-resolved thread is a no-op.\n- unresolve_thread: Unresolve a previously resolved review thread. Requires only \"threadId\" parameter. The owner, repo, and pullNumber parameters are not used for this method. Unresolving an already-unresolved thread is a no-op.\n",
66
"inputSchema": {

pkg/github/issue_fields.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
ghcontext "github.com/github/github-mcp-server/pkg/context"
9+
ghErrors "github.com/github/github-mcp-server/pkg/errors"
10+
"github.com/github/github-mcp-server/pkg/inventory"
11+
"github.com/github/github-mcp-server/pkg/scopes"
12+
"github.com/github/github-mcp-server/pkg/translations"
13+
"github.com/github/github-mcp-server/pkg/utils"
14+
"github.com/google/jsonschema-go/jsonschema"
15+
"github.com/modelcontextprotocol/go-sdk/mcp"
16+
"github.com/shurcooL/githubv4"
17+
)
18+
19+
// IssueField represents a repository issue field definition.
20+
type IssueField struct {
21+
ID string `json:"id"`
22+
Name string `json:"name"`
23+
Description string `json:"description,omitempty"`
24+
DataType string `json:"data_type"`
25+
Visibility string `json:"visibility"`
26+
Options []IssueSingleSelectFieldOption `json:"options,omitempty"`
27+
}
28+
29+
// IssueSingleSelectFieldOption represents an option for a single_select issue field.
30+
type IssueSingleSelectFieldOption struct {
31+
ID string `json:"id"`
32+
Name string `json:"name"`
33+
Description string `json:"description,omitempty"`
34+
Color string `json:"color"`
35+
Priority *int `json:"priority,omitempty"`
36+
}
37+
38+
// issueFieldNode is the GraphQL fragment for a single issue field in the IssueFields union.
39+
// Only the fragment matching __typename is populated; read from the matching fragment.
40+
type issueFieldNode struct {
41+
TypeName githubv4.String `graphql:"__typename"`
42+
IssueFieldText struct {
43+
ID githubv4.ID
44+
Name githubv4.String
45+
Description githubv4.String
46+
DataType githubv4.String
47+
Visibility githubv4.String
48+
} `graphql:"... on IssueFieldText"`
49+
IssueFieldNumber struct {
50+
ID githubv4.ID
51+
Name githubv4.String
52+
Description githubv4.String
53+
DataType githubv4.String
54+
Visibility githubv4.String
55+
} `graphql:"... on IssueFieldNumber"`
56+
IssueFieldDate struct {
57+
ID githubv4.ID
58+
Name githubv4.String
59+
Description githubv4.String
60+
DataType githubv4.String
61+
Visibility githubv4.String
62+
} `graphql:"... on IssueFieldDate"`
63+
IssueFieldSingleSelect struct {
64+
ID githubv4.ID
65+
Name githubv4.String
66+
Description githubv4.String
67+
DataType githubv4.String
68+
Visibility githubv4.String
69+
Options []struct {
70+
ID githubv4.ID
71+
Name githubv4.String
72+
Description githubv4.String
73+
Color githubv4.String
74+
Priority *int
75+
}
76+
} `graphql:"... on IssueFieldSingleSelect"`
77+
}
78+
79+
// issueFieldsRepoQuery is the GraphQL query for listing issue fields on a repository.
80+
type issueFieldsRepoQuery struct {
81+
Repository struct {
82+
IssueFields struct {
83+
Nodes []issueFieldNode
84+
} `graphql:"issueFields(first: 100)"`
85+
} `graphql:"repository(owner: $owner, name: $name)"`
86+
}
87+
88+
// issueFieldsOrgQuery is the GraphQL query for listing issue fields on an organization.
89+
type issueFieldsOrgQuery struct {
90+
Organization struct {
91+
IssueFields struct {
92+
Nodes []issueFieldNode
93+
} `graphql:"issueFields(first: 100)"`
94+
} `graphql:"organization(login: $login)"`
95+
}
96+
97+
// ListIssueFields creates a tool to list issue field definitions for a repository or organization.
98+
func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool {
99+
return NewTool(
100+
ToolsetMetadataIssues,
101+
mcp.Tool{
102+
Name: "list_issue_fields",
103+
Description: t("TOOL_LIST_ISSUE_FIELDS_DESCRIPTION", "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select), and for single_select fields the list of valid option names. When repo is omitted, returns org-level fields directly."),
104+
Annotations: &mcp.ToolAnnotations{
105+
Title: t("TOOL_LIST_ISSUE_FIELDS_USER_TITLE", "List issue fields"),
106+
ReadOnlyHint: true,
107+
},
108+
InputSchema: &jsonschema.Schema{
109+
Type: "object",
110+
Properties: map[string]*jsonschema.Schema{
111+
"owner": {
112+
Type: "string",
113+
Description: "The account owner of the repository or organization. The name is not case sensitive.",
114+
},
115+
"repo": {
116+
Type: "string",
117+
Description: "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
118+
},
119+
},
120+
Required: []string{"owner"},
121+
},
122+
},
123+
[]scopes.Scope{scopes.Repo, scopes.ReadOrg},
124+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
125+
owner, err := RequiredParam[string](args, "owner")
126+
if err != nil {
127+
return utils.NewToolResultError(err.Error()), nil, nil
128+
}
129+
repo, err := OptionalParam[string](args, "repo")
130+
if err != nil {
131+
return utils.NewToolResultError(err.Error()), nil, nil
132+
}
133+
134+
gqlClient, err := deps.GetGQLClient(ctx)
135+
if err != nil {
136+
return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
137+
}
138+
139+
fields, err := fetchIssueFields(ctx, gqlClient, owner, repo)
140+
if err != nil {
141+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
142+
}
143+
144+
r, err := json.Marshal(fields)
145+
if err != nil {
146+
return utils.NewToolResultErrorFromErr("failed to marshal issue fields", err), nil, nil
147+
}
148+
149+
return utils.NewToolResultText(string(r)), nil, nil
150+
})
151+
}
152+
153+
// fetchIssueFields returns the issue field definitions for the given owner.
154+
// If repo is provided, fields are scoped to that repository (inherited from its
155+
// organization); otherwise fields are returned directly from the organization.
156+
func fetchIssueFields(ctx context.Context, gqlClient *githubv4.Client, owner, repo string) ([]IssueField, error) {
157+
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
158+
if repo != "" {
159+
var query issueFieldsRepoQuery
160+
vars := map[string]any{
161+
"owner": githubv4.String(owner),
162+
"name": githubv4.String(repo),
163+
}
164+
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
165+
return nil, err
166+
}
167+
return issueFieldsFromNodes(query.Repository.IssueFields.Nodes), nil
168+
}
169+
170+
var query issueFieldsOrgQuery
171+
vars := map[string]any{
172+
"login": githubv4.String(owner),
173+
}
174+
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
175+
return nil, err
176+
}
177+
return issueFieldsFromNodes(query.Organization.IssueFields.Nodes), nil
178+
}
179+
180+
// issueFieldsFromNodes converts GraphQL issue field union nodes into IssueField values.
181+
// Read from the fragment matching __typename; the other fragments are zero-valued.
182+
func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
183+
fields := make([]IssueField, 0, len(nodes))
184+
for _, node := range nodes {
185+
var f IssueField
186+
switch string(node.TypeName) {
187+
case "IssueFieldSingleSelect":
188+
opts := make([]IssueSingleSelectFieldOption, 0, len(node.IssueFieldSingleSelect.Options))
189+
for _, o := range node.IssueFieldSingleSelect.Options {
190+
opts = append(opts, IssueSingleSelectFieldOption{
191+
ID: fmt.Sprintf("%v", o.ID),
192+
Name: string(o.Name),
193+
Description: string(o.Description),
194+
Color: string(o.Color),
195+
Priority: o.Priority,
196+
})
197+
}
198+
f = IssueField{
199+
ID: fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
200+
Name: string(node.IssueFieldSingleSelect.Name),
201+
Description: string(node.IssueFieldSingleSelect.Description),
202+
DataType: string(node.IssueFieldSingleSelect.DataType),
203+
Visibility: string(node.IssueFieldSingleSelect.Visibility),
204+
Options: opts,
205+
}
206+
case "IssueFieldText":
207+
f = IssueField{
208+
ID: fmt.Sprintf("%v", node.IssueFieldText.ID),
209+
Name: string(node.IssueFieldText.Name),
210+
Description: string(node.IssueFieldText.Description),
211+
DataType: string(node.IssueFieldText.DataType),
212+
Visibility: string(node.IssueFieldText.Visibility),
213+
}
214+
case "IssueFieldNumber":
215+
f = IssueField{
216+
ID: fmt.Sprintf("%v", node.IssueFieldNumber.ID),
217+
Name: string(node.IssueFieldNumber.Name),
218+
Description: string(node.IssueFieldNumber.Description),
219+
DataType: string(node.IssueFieldNumber.DataType),
220+
Visibility: string(node.IssueFieldNumber.Visibility),
221+
}
222+
case "IssueFieldDate":
223+
f = IssueField{
224+
ID: fmt.Sprintf("%v", node.IssueFieldDate.ID),
225+
Name: string(node.IssueFieldDate.Name),
226+
Description: string(node.IssueFieldDate.Description),
227+
DataType: string(node.IssueFieldDate.DataType),
228+
Visibility: string(node.IssueFieldDate.Visibility),
229+
}
230+
default:
231+
continue
232+
}
233+
fields = append(fields, f)
234+
}
235+
return fields
236+
}

0 commit comments

Comments
 (0)