Skip to content

Commit fe90c23

Browse files
authored
Add list project fields tool
1 parent 62bcbfc commit fe90c23

File tree

7 files changed

+310
-24
lines changed

7 files changed

+310
-24
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,18 @@ The following sets of tools are available (all are on by default):
659659
<summary>Projects</summary>
660660

661661
- **get_project** - Get project
662-
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive. (string, required)
662+
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required)
663663
- `owner_type`: Owner type (string, required)
664664
- `project_number`: The project's number (number, required)
665665

666+
- **list_project_fields** - List project fields
667+
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required)
668+
- `owner_type`: Owner type (string, required)
669+
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
670+
- `projectNumber`: The project's number. (string, required)
671+
666672
- **list_projects** - List projects
667-
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive. (string, required)
673+
- `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required)
668674
- `owner_type`: Owner type (string, required)
669675
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
670676
- `query`: Filter projects by a search query (matches title and description) (string, optional)

pkg/github/__toolsnaps__/get_project.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
"title": "Get project",
44
"readOnlyHint": true
55
},
6-
"description": "Get Project for an user or organization",
6+
"description": "Get Project for an user or org",
77
"inputSchema": {
88
"properties": {
99
"owner": {
10-
"description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive.",
10+
"description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.",
1111
"type": "string"
1212
},
1313
"owner_type": {
1414
"description": "Owner type",
1515
"enum": [
1616
"user",
17-
"organization"
17+
"org"
1818
],
1919
"type": "string"
2020
},
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"annotations": {
3+
"title": "List project fields",
4+
"readOnlyHint": true
5+
},
6+
"description": "List Project fields for an user or org",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.",
11+
"type": "string"
12+
},
13+
"owner_type": {
14+
"description": "Owner type",
15+
"enum": [
16+
"user",
17+
"org"
18+
],
19+
"type": "string"
20+
},
21+
"per_page": {
22+
"description": "Number of results per page (max 100, default: 30)",
23+
"type": "number"
24+
},
25+
"projectNumber": {
26+
"description": "The project's number.",
27+
"type": "string"
28+
}
29+
},
30+
"required": [
31+
"owner_type",
32+
"owner",
33+
"projectNumber"
34+
],
35+
"type": "object"
36+
},
37+
"name": "list_project_fields"
38+
}

pkg/github/__toolsnaps__/list_projects.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
"title": "List projects",
44
"readOnlyHint": true
55
},
6-
"description": "List Projects for a user or organization",
6+
"description": "List Projects for an user or org",
77
"inputSchema": {
88
"properties": {
99
"owner": {
10-
"description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive.",
10+
"description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.",
1111
"type": "string"
1212
},
1313
"owner_type": {
1414
"description": "Owner type",
1515
"enum": [
1616
"user",
17-
"organization"
17+
"org"
1818
],
1919
"type": "string"
2020
},

pkg/github/projects.go

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import (
1919

2020
func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
2121
return mcp.NewTool("list_projects",
22-
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for a user or organization")),
22+
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for an user or org")),
2323
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), ReadOnlyHint: ToBoolPtr(true)}),
24-
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "organization")),
25-
mcp.WithString("owner", mcp.Required(), mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive.")),
24+
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
25+
mcp.WithString("owner", mcp.Required(), mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.")),
2626
mcp.WithString("query", mcp.Description("Filter projects by a search query (matches title and description)")),
2727
mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
2828
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -48,7 +48,7 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
4848
}
4949

5050
var url string
51-
if ownerType == "organization" {
51+
if ownerType == "org" {
5252
url = fmt.Sprintf("orgs/%s/projectsV2", owner)
5353
} else {
5454
url = fmt.Sprintf("users/%s/projectsV2", owner)
@@ -101,11 +101,11 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
101101

102102
func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
103103
return mcp.NewTool("get_project",
104-
mcp.WithDescription(t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for an user or organization")),
104+
mcp.WithDescription(t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for an user or org")),
105105
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"), ReadOnlyHint: ToBoolPtr(true)}),
106106
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number")),
107-
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "organization")),
108-
mcp.WithString("owner", mcp.Required(), mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == organization it is the name of the organization. The name is not case sensitive.")),
107+
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
108+
mcp.WithString("owner", mcp.Required(), mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.")),
109109
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
110110

111111
projectNumber, err := RequiredInt(req, "project_number")
@@ -129,7 +129,7 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (to
129129
}
130130

131131
var url string
132-
if ownerType == "organization" {
132+
if ownerType == "org" {
133133
url = fmt.Sprintf("orgs/%s/projectsV2/%d", owner, projectNumber)
134134
} else {
135135
url = fmt.Sprintf("users/%s/projectsV2/%d", owner, projectNumber)
@@ -168,6 +168,96 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (to
168168
}
169169
}
170170

171+
func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
172+
return mcp.NewTool("list_project_fields",
173+
mcp.WithDescription(t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for an user or org")),
174+
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"), ReadOnlyHint: ToBoolPtr(true)}),
175+
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
176+
mcp.WithString("owner", mcp.Required(), mcp.Description("If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.")),
177+
mcp.WithString("projectNumber", mcp.Required(), mcp.Description("The project's number.")),
178+
mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
179+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
180+
owner, err := RequiredParam[string](req, "owner")
181+
if err != nil {
182+
return mcp.NewToolResultError(err.Error()), nil
183+
}
184+
ownerType, err := RequiredParam[string](req, "owner_type")
185+
if err != nil {
186+
return mcp.NewToolResultError(err.Error()), nil
187+
}
188+
projectNumber, err := RequiredParam[string](req, "projectNumber")
189+
if err != nil {
190+
return mcp.NewToolResultError(err.Error()), nil
191+
}
192+
perPage, err := OptionalIntParamWithDefault(req, "per_page", 30)
193+
if err != nil {
194+
return mcp.NewToolResultError(err.Error()), nil
195+
}
196+
client, err := getClient(ctx)
197+
if err != nil {
198+
return mcp.NewToolResultError(err.Error()), nil
199+
}
200+
201+
var url string
202+
if ownerType == "org" {
203+
url = fmt.Sprintf("orgs/%s/projectsV2/%s/fields", owner, projectNumber)
204+
} else {
205+
url = fmt.Sprintf("users/%s/projectsV2/%s/fields", owner, projectNumber)
206+
}
207+
projectFields := []projectV2Field{}
208+
209+
opts := listProjectsOptions{PerPage: perPage}
210+
211+
if perPage > 0 {
212+
opts.PerPage = perPage
213+
}
214+
url, err = addOptions(url, opts)
215+
if err != nil {
216+
return nil, fmt.Errorf("failed to add options to request: %w", err)
217+
}
218+
219+
httpRequest, err := client.NewRequest("GET", url, nil)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to create request: %w", err)
222+
}
223+
224+
resp, err := client.Do(ctx, httpRequest, &projectFields)
225+
if err != nil {
226+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
227+
"failed to list projects",
228+
resp,
229+
err,
230+
), nil
231+
}
232+
defer func() { _ = resp.Body.Close() }()
233+
234+
if resp.StatusCode != http.StatusOK {
235+
body, err := io.ReadAll(resp.Body)
236+
if err != nil {
237+
return nil, fmt.Errorf("failed to read response body: %w", err)
238+
}
239+
return mcp.NewToolResultError(fmt.Sprintf("failed to list projects: %s", string(body))), nil
240+
}
241+
r, err := json.Marshal(projectFields)
242+
if err != nil {
243+
return nil, fmt.Errorf("failed to marshal response: %w", err)
244+
}
245+
246+
return mcp.NewToolResultText(string(r)), nil
247+
}
248+
}
249+
250+
type projectV2Field struct {
251+
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
252+
NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
253+
Name string `json:"name,omitempty"` // The display name of the field.
254+
DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select").
255+
URL string `json:"url,omitempty"` // The API URL for this field.
256+
Options []*any `json:"options,omitempty"` // Available options for single_select and multi_select fields.
257+
CreatedAt *github.Timestamp `json:"created_at,omitempty"` // The time when this field was created.
258+
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated.
259+
}
260+
171261
type listProjectsOptions struct {
172262
// For paginated result sets, the number of results to include per page.
173263
PerPage int `url:"per_page,omitempty"`

0 commit comments

Comments
 (0)