Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ The following sets of tools are available (all are on by default):
| `issues` | GitHub Issues related tools |
| `notifications` | GitHub Notifications related tools |
| `orgs` | GitHub Organization related tools |
| `projects` | GitHub Projects related tools |
| `pull_requests` | GitHub Pull Request related tools |
| `repos` | GitHub Repository related tools |
| `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
Expand Down Expand Up @@ -655,6 +656,20 @@ The following sets of tools are available (all are on by default):

<details>

<summary>Projects</summary>

- **list_projects** - List projects
- `after`: Cursor for items after (forward pagination) (string, optional)
- `before`: Cursor for items before (backwards pagination) (string, optional)
- `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)
- `owner_type`: Owner type (string, required)
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
- `query`: Filter projects by a search query (matches title and description) (string, optional)

</details>

<details>

<summary>Pull Requests</summary>

- **add_comment_to_pending_review** - Add review comment to the requester's latest pending pull request review
Expand Down
1 change: 1 addition & 0 deletions docs/remote-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| 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) |
| 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) |
| 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) |
| 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) |
| 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) |
| 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) |
| 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) |
Expand Down
45 changes: 45 additions & 0 deletions pkg/github/__toolsnaps__/list_projects.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"annotations": {
"title": "List projects",
"readOnlyHint": true
},
"description": "List Projects for a user or organization",
"inputSchema": {
"properties": {
"after": {
"description": "Cursor for items after (forward pagination)",
"type": "string"
},
"before": {
"description": "Cursor for items before (backwards pagination)",
"type": "string"
},
"owner": {
"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.",
"type": "string"
},
"owner_type": {
"description": "Owner type",
"enum": [
"user",
"organization"
],
"type": "string"
},
"per_page": {
"description": "Number of results per page (max 100, default: 30)",
"type": "number"
},
"query": {
"description": "Filter projects by a search query (matches title and description)",
"type": "string"
}
},
"required": [
"owner_type",
"owner"
],
"type": "object"
},
"name": "list_projects"
}
150 changes: 150 additions & 0 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package github

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v74/github"
"github.com/google/go-querystring/query"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The code duplicates the addOptions function that already exists in the go-github library. Consider using github.addOptions from the go-github package instead of implementing a custom version to reduce code duplication.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes genius, it exists in go-github but is not exported.

)

func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_projects",
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", "List Projects for a user or organization")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), ReadOnlyHint: ToBoolPtr(true)}),
mcp.WithString("owner_type", mcp.Required(), mcp.Description("Owner type"), mcp.Enum("user", "organization")),
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.")),
mcp.WithString("query", mcp.Description("Filter projects by a search query (matches title and description)")),
mcp.WithString("before", mcp.Description("Cursor for items before (backwards pagination)")),
mcp.WithString("after", mcp.Description("Cursor for items after (forward pagination)")),
mcp.WithNumber("per_page", mcp.Description("Number of results per page (max 100, default: 30)")),
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := RequiredParam[string](req, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
ownerType, err := RequiredParam[string](req, "owner_type")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
queryStr, err := OptionalParam[string](req, "query")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

beforeCursor, err := OptionalParam[string](req, "before")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
afterCursor, err := OptionalParam[string](req, "after")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
perPage, err := OptionalIntParamWithDefault(req, "per_page", 30)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

client, err := getClient(ctx)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

var url string
if ownerType == "organization" {
url = fmt.Sprintf("/orgs/%s/projectsV2", owner)
} else {
url = fmt.Sprintf("/users/%s/projectsV2", owner)
}
projects := []github.ProjectV2{}

opts := ListProjectsOptions{PerPage: perPage}
if afterCursor != "" {
opts.After = afterCursor
}
if beforeCursor != "" {
opts.Before = beforeCursor
}
if queryStr != "" {
opts.Query = queryStr
}
url, err = addOptions(url, opts)
if err != nil {
return nil, fmt.Errorf("failed to add options to request: %w", err)
}

httpRequest, err := client.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

resp, err := client.Do(ctx, httpRequest, &projects)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to list projects",
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to list projects: %s", string(body))), nil
}
r, err := json.Marshal(projects)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

type ListProjectsOptions struct {
// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
Before string `url:"before,omitempty"`

// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
After string `url:"after,omitempty"`

// For paginated result sets, the number of results to include per page.
PerPage int `url:"per_page,omitempty"`

// Query Limit results to projects of the specified type.
Query string `url:"q,omitempty"`
}

// addOptions adds the parameters in opts as URL query parameters to s. opts
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opts any) (string, error) {
v := reflect.ValueOf(opts)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}

u, err := url.Parse(s)
if err != nil {
return s, err
}

qs, err := query.Values(opts)
if err != nil {
return s, err
}

u.RawQuery = qs.Encode()
return u.String(), nil
}
Loading
Loading