Skip to content

Commit ad5f75c

Browse files
committed
Add list_project_items tool
1 parent 822c930 commit ad5f75c

File tree

2 files changed

+103
-1
lines changed

2 files changed

+103
-1
lines changed

pkg/github/projects.go

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (
3434
if err != nil {
3535
return mcp.NewToolResultError(err.Error()), nil
3636
}
37-
queryStr, err := OptionalParam[string](req, "query")
37+
queryStr, err := OptionalParam[string](req, "q")
3838
if err != nil {
3939
return mcp.NewToolResultError(err.Error()), nil
4040
}
@@ -329,6 +329,92 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
329329
}
330330
}
331331

332+
func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
333+
return mcp.NewTool("list_project_items",
334+
mcp.WithDescription(t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", "List Project items for a user or org")),
335+
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), ReadOnlyHint: ToBoolPtr(true)}),
336+
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "org")),
337+
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.")),
338+
mcp.WithNumber("project_number", mcp.Required(), mcp.Description("The project's number.")),
339+
mcp.WithString("query", mcp.Description("Search query to filter items")),
340+
mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
341+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
342+
owner, err := RequiredParam[string](req, "owner")
343+
if err != nil {
344+
return mcp.NewToolResultError(err.Error()), nil
345+
}
346+
ownerType, err := RequiredParam[string](req, "owner_type")
347+
if err != nil {
348+
return mcp.NewToolResultError(err.Error()), nil
349+
}
350+
projectNumber, err := RequiredInt(req, "project_number")
351+
if err != nil {
352+
return mcp.NewToolResultError(err.Error()), nil
353+
}
354+
perPage, err := OptionalIntParamWithDefault(req, "per_page", 30)
355+
if err != nil {
356+
return mcp.NewToolResultError(err.Error()), nil
357+
}
358+
queryStr, err := OptionalParam[string](req, "q")
359+
if err != nil {
360+
return mcp.NewToolResultError(err.Error()), nil
361+
}
362+
client, err := getClient(ctx)
363+
if err != nil {
364+
return mcp.NewToolResultError(err.Error()), nil
365+
}
366+
367+
var url string
368+
if ownerType == "org" {
369+
url = fmt.Sprintf("orgs/%s/projectsV2/%d/items", owner, projectNumber)
370+
} else {
371+
url = fmt.Sprintf("users/%s/projectsV2/%d/items", owner, projectNumber)
372+
}
373+
projectItems := []projectV2Item{}
374+
375+
opts := listProjectsOptions{PerPage: perPage}
376+
if queryStr != "" {
377+
opts.Query = queryStr
378+
}
379+
if perPage > 0 {
380+
opts.PerPage = perPage
381+
}
382+
url, err = addOptions(url, opts)
383+
if err != nil {
384+
return nil, fmt.Errorf("failed to add options to request: %w", err)
385+
}
386+
387+
httpRequest, err := client.NewRequest("GET", url, nil)
388+
if err != nil {
389+
return nil, fmt.Errorf("failed to create request: %w", err)
390+
}
391+
392+
resp, err := client.Do(ctx, httpRequest, &projectItems)
393+
if err != nil {
394+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
395+
"failed to list project items",
396+
resp,
397+
err,
398+
), nil
399+
}
400+
defer func() { _ = resp.Body.Close() }()
401+
402+
if resp.StatusCode != http.StatusOK {
403+
body, err := io.ReadAll(resp.Body)
404+
if err != nil {
405+
return nil, fmt.Errorf("failed to read response body: %w", err)
406+
}
407+
return mcp.NewToolResultError(fmt.Sprintf("failed to list projects: %s", string(body))), nil
408+
}
409+
r, err := json.Marshal(projectItems)
410+
if err != nil {
411+
return nil, fmt.Errorf("failed to marshal response: %w", err)
412+
}
413+
414+
return mcp.NewToolResultText(string(r)), nil
415+
}
416+
}
417+
332418
type projectV2Field struct {
333419
ID *int64 `json:"id,omitempty"` // The unique identifier for this field.
334420
NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field.
@@ -340,6 +426,21 @@ type projectV2Field struct {
340426
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated.
341427
}
342428

429+
type projectV2Item struct {
430+
ID *int64 `json:"id,omitempty"`
431+
NodeID *string `json:"node_id,omitempty"`
432+
ProjectNodeID *string `json:"project_node_id,omitempty"`
433+
ContentNodeID *string `json:"content_node_id,omitempty"`
434+
ProjectURL *string `json:"project_url,omitempty"`
435+
ContentType *string `json:"content_type,omitempty"`
436+
Creator *github.User `json:"creator,omitempty"`
437+
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
438+
UpdatedAt *github.Timestamp `json:"updated_at,omitempty"`
439+
ArchivedAt *github.Timestamp `json:"archived_at,omitempty"`
440+
ItemURL *string `json:"item_url,omitempty"`
441+
Fields []*projectV2Field `json:"fields,omitempty"`
442+
}
443+
343444
type listProjectsOptions struct {
344445
// For paginated result sets, the number of results to include per page.
345446
PerPage int `url:"per_page,omitempty"`

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
196196
toolsets.NewServerTool(GetProject(getClient, t)),
197197
toolsets.NewServerTool(ListProjectFields(getClient, t)),
198198
toolsets.NewServerTool(GetProjectField(getClient, t)),
199+
toolsets.NewServerTool(ListProjectItems(getClient, t)),
199200
)
200201

201202
// Add toolsets to the group

0 commit comments

Comments
 (0)