Skip to content

Commit 23630b3

Browse files
authored
Add ListProjects tool (#1113)
* Bump go-viper/mapstructure * Update github.com/go-viper/mapstructure/v2 version in licenses * Add tool to list projects * Fix ordering
1 parent d6d60f4 commit 23630b3

File tree

6 files changed

+385
-0
lines changed

6 files changed

+385
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ The following sets of tools are available (all are on by default):
288288
| `issues` | GitHub Issues related tools |
289289
| `notifications` | GitHub Notifications related tools |
290290
| `orgs` | GitHub Organization related tools |
291+
| `projects` | GitHub Projects related tools |
291292
| `pull_requests` | GitHub Pull Request related tools |
292293
| `repos` | GitHub Repository related tools |
293294
| `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
@@ -655,6 +656,20 @@ The following sets of tools are available (all are on by default):
655656

656657
<details>
657658

659+
<summary>Projects</summary>
660+
661+
- **list_projects** - List projects
662+
- `after`: Cursor for items after (forward pagination) (string, optional)
663+
- `before`: Cursor for items before (backwards pagination) (string, optional)
664+
- `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)
665+
- `owner_type`: Owner type (string, required)
666+
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
667+
- `query`: Filter projects by a search query (matches title and description) (string, optional)
668+
669+
</details>
670+
671+
<details>
672+
658673
<summary>Pull Requests</summary>
659674

660675
- **add_comment_to_pending_review** - Add review comment to the requester's latest pending pull request review

docs/remote-server.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
2929
| Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
3030
| Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) |
3131
| Organizations | GitHub Organization related tools | https://api.githubcopilot.com/mcp/x/orgs | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/orgs/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%2Freadonly%22%7D) |
32+
| Projects | GitHub Projects related tools | https://api.githubcopilot.com/mcp/x/projects | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-projects&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fprojects%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/projects/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-projects&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fprojects%2Freadonly%22%7D) |
3233
| Pull Requests | GitHub Pull Request related tools | https://api.githubcopilot.com/mcp/x/pull_requests | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/pull_requests/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%2Freadonly%22%7D) |
3334
| Repositories | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) |
3435
| Secret Protection | Secret protection related tools, such as GitHub Secret Scanning | https://api.githubcopilot.com/mcp/x/secret_protection | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/secret_protection/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%2Freadonly%22%7D) |
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"annotations": {
3+
"title": "List projects",
4+
"readOnlyHint": true
5+
},
6+
"description": "List Projects for a user or organization",
7+
"inputSchema": {
8+
"properties": {
9+
"after": {
10+
"description": "Cursor for items after (forward pagination)",
11+
"type": "string"
12+
},
13+
"before": {
14+
"description": "Cursor for items before (backwards pagination)",
15+
"type": "string"
16+
},
17+
"owner": {
18+
"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.",
19+
"type": "string"
20+
},
21+
"owner_type": {
22+
"description": "Owner type",
23+
"enum": [
24+
"user",
25+
"organization"
26+
],
27+
"type": "string"
28+
},
29+
"per_page": {
30+
"description": "Number of results per page (max 100, default: 30)",
31+
"type": "number"
32+
},
33+
"query": {
34+
"description": "Filter projects by a search query (matches title and description)",
35+
"type": "string"
36+
}
37+
},
38+
"required": [
39+
"owner_type",
40+
"owner"
41+
],
42+
"type": "object"
43+
},
44+
"name": "list_projects"
45+
}

pkg/github/projects.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"reflect"
11+
12+
ghErrors "github.com/github/github-mcp-server/pkg/errors"
13+
"github.com/github/github-mcp-server/pkg/translations"
14+
"github.com/google/go-github/v74/github"
15+
"github.com/google/go-querystring/query"
16+
"github.com/mark3labs/mcp-go/mcp"
17+
"github.com/mark3labs/mcp-go/server"
18+
)
19+
20+
func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
21+
return mcp.NewTool("list_projects",
22+
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for a user or organization")),
23+
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.")),
26+
mcp.WithString("query", mcp.Description("Filter projects by a search query (matches title and description)")),
27+
mcp.WithString("before", mcp.Description("Cursor for items before (backwards pagination)")),
28+
mcp.WithString("after", mcp.Description("Cursor for items after (forward pagination)")),
29+
mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
30+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
31+
owner, err := RequiredParam[string](req, "owner")
32+
if err != nil {
33+
return mcp.NewToolResultError(err.Error()), nil
34+
}
35+
ownerType, err := RequiredParam[string](req, "owner_type")
36+
if err != nil {
37+
return mcp.NewToolResultError(err.Error()), nil
38+
}
39+
queryStr, err := OptionalParam[string](req, "query")
40+
if err != nil {
41+
return mcp.NewToolResultError(err.Error()), nil
42+
}
43+
44+
beforeCursor, err := OptionalParam[string](req, "before")
45+
if err != nil {
46+
return mcp.NewToolResultError(err.Error()), nil
47+
}
48+
afterCursor, err := OptionalParam[string](req, "after")
49+
if err != nil {
50+
return mcp.NewToolResultError(err.Error()), nil
51+
}
52+
perPage, err := OptionalIntParamWithDefault(req, "per_page", 30)
53+
if err != nil {
54+
return mcp.NewToolResultError(err.Error()), nil
55+
}
56+
57+
client, err := getClient(ctx)
58+
if err != nil {
59+
return mcp.NewToolResultError(err.Error()), nil
60+
}
61+
62+
var url string
63+
if ownerType == "organization" {
64+
url = fmt.Sprintf("/orgs/%s/projectsV2", owner)
65+
} else {
66+
url = fmt.Sprintf("/users/%s/projectsV2", owner)
67+
}
68+
projects := []github.ProjectV2{}
69+
70+
opts := ListProjectsOptions{PerPage: perPage}
71+
if afterCursor != "" {
72+
opts.After = afterCursor
73+
}
74+
if beforeCursor != "" {
75+
opts.Before = beforeCursor
76+
}
77+
if queryStr != "" {
78+
opts.Query = queryStr
79+
}
80+
url, err = addOptions(url, opts)
81+
if err != nil {
82+
return nil, fmt.Errorf("failed to add options to request: %w", err)
83+
}
84+
85+
httpRequest, err := client.NewRequest("GET", url, nil)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to create request: %w", err)
88+
}
89+
90+
resp, err := client.Do(ctx, httpRequest, &projects)
91+
if err != nil {
92+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
93+
"failed to list projects",
94+
resp,
95+
err,
96+
), nil
97+
}
98+
defer func() { _ = resp.Body.Close() }()
99+
100+
if resp.StatusCode != http.StatusOK {
101+
body, err := io.ReadAll(resp.Body)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to read response body: %w", err)
104+
}
105+
return mcp.NewToolResultError(fmt.Sprintf("failed to list projects: %s", string(body))), nil
106+
}
107+
r, err := json.Marshal(projects)
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to marshal response: %w", err)
110+
}
111+
112+
return mcp.NewToolResultText(string(r)), nil
113+
}
114+
}
115+
116+
type ListProjectsOptions struct {
117+
// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
118+
Before string `url:"before,omitempty"`
119+
120+
// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
121+
After string `url:"after,omitempty"`
122+
123+
// For paginated result sets, the number of results to include per page.
124+
PerPage int `url:"per_page,omitempty"`
125+
126+
// Query Limit results to projects of the specified type.
127+
Query string `url:"q,omitempty"`
128+
}
129+
130+
// addOptions adds the parameters in opts as URL query parameters to s. opts
131+
// must be a struct whose fields may contain "url" tags.
132+
func addOptions(s string, opts any) (string, error) {
133+
v := reflect.ValueOf(opts)
134+
if v.Kind() == reflect.Ptr && v.IsNil() {
135+
return s, nil
136+
}
137+
138+
u, err := url.Parse(s)
139+
if err != nil {
140+
return s, err
141+
}
142+
143+
qs, err := query.Values(opts)
144+
if err != nil {
145+
return s, err
146+
}
147+
148+
u.RawQuery = qs.Encode()
149+
return u.String(), nil
150+
}

0 commit comments

Comments
 (0)