Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add support for Repository Discussions#459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
xcorail wants to merge5 commits intogithub:main
base:main
Choose a base branch
Loading
fromxcorail:main
Open
Show file tree
Hide file tree
Changes fromall 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
27 changes: 27 additions & 0 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,7 @@ The following sets of tools are available (all are on by default):
| ----------------------- | ------------------------------------------------------------- |
| `repos` | Repository-related tools (file operations, branches, commits) |
| `issues` | Issue-related tools (create, read, update, comment) |
| `discussions` | GitHub Discussions tools (list, get, comments, categories) |
| `users` | Anything relating to GitHub Users |
| `pull_requests` | Pull request operations (create, merge, review) |
| `code_security` | Code scanning alerts and security features |
Expand DownExpand Up@@ -614,6 +615,32 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `repo`: The name of the repository (string, required)
- `action`: Action to perform: `ignore`, `watch`, or `delete` (string, required)

### Discussions

- **list_discussions** - List discussions for a repository
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `categoryId`: Filter by category ID (string, optional)
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
- `first`: Pagination - Number of records to retrieve (number, optional)
- `after`: Pagination - Cursor to start with (string, optional)
- `sort`: Sort by ('CREATED_AT', 'UPDATED_AT') (string, optional)
- `direction`: Sort direction ('ASC', 'DESC') (string, optional)

- **get_discussion** - Get a specific discussion by ID
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `discussionNumber`: Discussion number (required)

- **get_discussion_comments** - Get comments from a discussion
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `discussionNumber`: Discussion number (required)

- **list_discussion_categories** - List discussion categories for a repository, with their IDs and names
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

## Resources

### Repository Content
Expand Down
333 changes: 333 additions & 0 deletionspkg/github/discussions.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
package github

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/github/github-mcp-server/pkg/translations"
"github.com/go-viper/mapstructure/v2"
"github.com/google/go-github/v69/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
)

func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_discussions",
mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_DISCUSSIONS_USER_TITLE", "List discussions"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithString("categoryId",
mcp.Description("Category ID filter"),
),
mcp.WithString("since",
mcp.Description("Filter by date (ISO 8601 timestamp)"),
),
mcp.WithString("sort",
mcp.Description("Sort field"),
mcp.DefaultString("CREATED_AT"),
mcp.Enum("CREATED_AT", "UPDATED_AT"),
),
mcp.WithString("direction",
mcp.Description("Sort direction"),
mcp.DefaultString("DESC"),
mcp.Enum("ASC", "DESC"),
),
mcp.WithNumber("first",
mcp.Description("Number of discussions to return per page (min 1, max 100)"),
mcp.Min(1),
mcp.Max(100),
),
mcp.WithString("after",
mcp.Description("Cursor for pagination, use the 'after' field from the previous response"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Decode params
var params struct {
Owner string
Repo string
CategoryID string
Since string
Sort string
Direction string
First int32
After string
}
if err := mapstructure.Decode(request.Params.Arguments, &params); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Get GraphQL client
client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
}
// Prepare GraphQL query
var q struct {
Repository struct {
Discussions struct {
Nodes []struct {
Number githubv4.Int
Title githubv4.String
CreatedAt githubv4.DateTime
Category struct {
Name githubv4.String
} `graphql:"category"`
URL githubv4.String `graphql:"url"`
}
} `graphql:"discussions(categoryId: $categoryId, orderBy: {field: $sort, direction: $direction}, first: $first, after: $after)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
// Build query variables
vars := map[string]interface{}{
"owner": githubv4.String(params.Owner),
"repo": githubv4.String(params.Repo),
"categoryId": githubv4.ID(params.CategoryID),
"sort": githubv4.DiscussionOrderField(params.Sort),
"direction": githubv4.OrderDirection(params.Direction),
"first": githubv4.Int(params.First),
"after": githubv4.String(params.After),
}
// Execute query
if err := client.Query(ctx, &q, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Map nodes to GitHub Issue objects - there is no discussion type in the GitHub API, so we use Issue to benefit from existing code
var discussions []*github.Issue
for _, n := range q.Repository.Discussions.Nodes {
di := &github.Issue{
Number: github.Ptr(int(n.Number)),
Title: github.Ptr(string(n.Title)),
HTMLURL: github.Ptr(string(n.URL)),
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
}
discussions = append(discussions, di)
}

// Post filtering discussions based on 'since' parameter
if params.Since != "" {
sinceTime, err := time.Parse(time.RFC3339, params.Since)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid 'since' timestamp: %v", err)), nil
}
var filteredDiscussions []*github.Issue
for _, d := range discussions {
if d.CreatedAt.Time.After(sinceTime) {
filteredDiscussions = append(filteredDiscussions, d)
}
}
discussions = filteredDiscussions
}

// Marshal and return
out, err := json.Marshal(discussions)
if err != nil {
return nil, fmt.Errorf("failed to marshal discussions: %w", err)
}
return mcp.NewToolResultText(string(out)), nil
}
}

func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_discussion",
mcp.WithDescription(t("TOOL_GET_DISCUSSION_DESCRIPTION", "Get a specific discussion by ID")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_DISCUSSION_USER_TITLE", "Get discussion"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithNumber("discussionNumber",
mcp.Required(),
mcp.Description("Discussion Number"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Decode params
var params struct {
Owner string
Repo string
DiscussionNumber int32
}
if err := mapstructure.Decode(request.Params.Arguments, &params); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
}

var q struct {
Repository struct {
Discussion struct {
Number githubv4.Int
Body githubv4.String
State githubv4.String
CreatedAt githubv4.DateTime
URL githubv4.String `graphql:"url"`
} `graphql:"discussion(number: $discussionNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(params.Owner),
"repo": githubv4.String(params.Repo),
"discussionNumber": githubv4.Int(params.DiscussionNumber),
}
if err := client.Query(ctx, &q, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
d := q.Repository.Discussion
discussion := &github.Issue{
Number: github.Ptr(int(d.Number)),
Body: github.Ptr(string(d.Body)),
State: github.Ptr(string(d.State)),
HTMLURL: github.Ptr(string(d.URL)),
CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time},
}
out, err := json.Marshal(discussion)
if err != nil {
return nil, fmt.Errorf("failed to marshal discussion: %w", err)
}

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

func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_discussion_comments",
mcp.WithDescription(t("TOOL_GET_DISCUSSION_COMMENTS_DESCRIPTION", "Get comments from a discussion")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_GET_DISCUSSION_COMMENTS_USER_TITLE", "Get discussion comments"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name")),
mcp.WithNumber("discussionNumber", mcp.Required(), mcp.Description("Discussion Number")),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Decode params
var params struct {
Owner string
Repo string
DiscussionNumber int32
}
if err := mapstructure.Decode(request.Params.Arguments, &params); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
}

var q struct {
Repository struct {
Discussion struct {
Comments struct {
Nodes []struct {
Body githubv4.String
}
} `graphql:"comments(first:100)"`
} `graphql:"discussion(number: $discussionNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(params.Owner),
"repo": githubv4.String(params.Repo),
"discussionNumber": githubv4.Int(params.DiscussionNumber),
}
if err := client.Query(ctx, &q, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
var comments []*github.IssueComment
for _, c := range q.Repository.Discussion.Comments.Nodes {
comments = append(comments, &github.IssueComment{Body: github.Ptr(string(c.Body))})
}

out, err := json.Marshal(comments)
if err != nil {
return nil, fmt.Errorf("failed to marshal comments: %w", err)
}

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

func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_discussion_categories",
mcp.WithDescription(t("TOOL_LIST_DISCUSSION_CATEGORIES_DESCRIPTION", "List discussion categories with their id and name, for a repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_DISCUSSION_CATEGORIES_USER_TITLE", "List discussion categories"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := requiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
}
var q struct {
Repository struct {
DiscussionCategories struct {
Nodes []struct {
ID githubv4.ID
Name githubv4.String
}
} `graphql:"discussionCategories(first: 30)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
}
if err := client.Query(ctx, &q, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
var categories []map[string]string
for _, c := range q.Repository.DiscussionCategories.Nodes {
categories = append(categories, map[string]string{
"id": fmt.Sprint(c.ID),
"name": string(c.Name),
})
}
out, err := json.Marshal(categories)
if err != nil {
return nil, fmt.Errorf("failed to marshal discussion categories: %w", err)
}
return mcp.NewToolResultText(string(out)), nil
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp