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

feat: implement get_repository_discussions tool with GraphQL support#261

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

Draft
sridharavinash wants to merge3 commits intomain
base:main
Choose a base branch
Loading
fromdiscussions-tooling
Draft
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
25 changes: 23 additions & 2 deletionscmd/github-mcp-server/main.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,9 +15,11 @@ import (
gogithub "github.com/google/go-github/v69/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)

var version = "version"
Expand DownExpand Up@@ -119,9 +121,20 @@ func runStdioServer(cfg runConfig) error {
if token == "" {
cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set")
}
ghClient := gogithub.NewClient(nil).WithAuthToken(token)

// Create OAuth2 token source
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
httpClient := oauth2.NewClient(ctx, ts)

// Create REST API client
ghClient := gogithub.NewClient(httpClient)
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version)

// Create GraphQL client
graphqlClient := githubv4.NewClient(httpClient)

// Check GH_HOST env var first, then fall back to viper config
host := os.Getenv("GH_HOST")
if host == "" {
Expand All@@ -134,6 +147,9 @@ func runStdioServer(cfg runConfig) error {
if err != nil {
return fmt.Errorf("failed to create GitHub client with host: %w", err)
}

// Also update GraphQL endpoint for enterprise if needed
graphqlClient = githubv4.NewEnterpriseClient(fmt.Sprintf("https://%s/api/graphql", host), httpClient)
}

t, dumpTranslations := translations.TranslationHelper()
Expand All@@ -146,11 +162,16 @@ func runStdioServer(cfg runConfig) error {
return ghClient, nil // closing over client
}

// Add function to get GraphQL client
getGraphQLClient := func(_ context.Context) (*githubv4.Client, error) {
return graphqlClient, nil // closing over graphql client
}

hooks := &server.Hooks{
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
}
// Create
ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks))
ghServer := github.NewServer(getClient,getGraphQLClient,version, cfg.readOnly, t, server.WithHooks(hooks))
stdioServer := server.NewStdioServer(ghServer)

stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0)
Expand Down
3 changes: 3 additions & 0 deletionsgo.mod
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,12 @@ require (
github.com/google/go-github/v69 v69.2.0
github.com/mark3labs/mcp-go v0.18.0
github.com/migueleliasweb/go-github-mock v1.1.0
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
golang.org/x/oauth2 v0.29.0
)

require (
Expand DownExpand Up@@ -41,6 +43,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletionsgo.sum
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,6 +83,10 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
Expand DownExpand Up@@ -138,6 +142,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
260 changes: 260 additions & 0 deletionspkg/github/discussions.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
package github

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

"github.com/github/github-mcp-server/pkg/translations"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
)

// Comment represents a comment on a GitHub Discussion
type Comment struct {
ID string `json:"id"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
Author string `json:"author"`
}

// Discussion represents a GitHub Discussion with its essential fields
type Discussion struct {
ID string `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
URL string `json:"url"`
Category string `json:"category"`
Author string `json:"author"`
Locked bool `json:"locked"`
UpvoteCount int `json:"upvoteCount"`
CommentCount int `json:"commentCount"`
Comments []Comment `json:"comments,omitempty"`

Choose a reason for hiding this comment

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

Really glad we would be able to also have access to the comments

sridharavinash reacted with heart emoji
}

// GetRepositoryDiscussions creates a tool to fetch discussions from a specific repository.
func GetRepositoryDiscussions(getGraphQLClient GetGraphQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_repository_discussions",
mcp.WithDescription(t("TOOL_GET_REPOSITORY_DISCUSSIONS_DESCRIPTION", "Get discussions from a specific GitHub repository")),
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
}

categoryId, err := OptionalParam[string](request, "categoryId")

Check failure on line 63 in pkg/github/discussions.go

View workflow job for this annotation

GitHub Actions/ lint

var-naming: var categoryId should be categoryID (revive)

Check failure on line 63 in pkg/github/discussions.go

View workflow job for this annotation

GitHub Actions/ lint

var-naming: var categoryId should be categoryID (revive)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Get GraphQL client
client, err := getGraphQLClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
}

// Define GraphQL query variables
variables := map[string]interface{}{
"owner": githubv4.String(owner),
"name": githubv4.String(repo),
"first": githubv4.Int(pagination.perPage),

Check failure on line 83 in pkg/github/discussions.go

View workflow job for this annotation

GitHub Actions/ lint

G115: integer overflow conversion int -> int32 (gosec)

Check failure on line 83 in pkg/github/discussions.go

View workflow job for this annotation

GitHub Actions/ lint

G115: integer overflow conversion int -> int32 (gosec)
"after": (*githubv4.String)(nil), // For pagination - null means first page
}

// For pagination beyond the first page
// TODO Fix
if pagination.page > 1 {
// We'd need an actual cursor here, but for simplicity we'll compute a rough offset
// In real implementation, you should store and use actual cursor values
cursorStr := githubv4.String(fmt.Sprintf("%d", (pagination.page-1)*pagination.perPage))
variables["after"] = &cursorStr
}

// Define the GraphQL query structure and query string based on whether categoryId is provided
var query struct {
Repository struct {
Discussions struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Number int
Title string
Body string
CreatedAt githubv4.DateTime
UpdatedAt githubv4.DateTime
URL githubv4.URI
Category struct {
Name string
}
Author struct {
Login string
}
Locked bool
UpvoteCount int
Comments struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Body string
CreatedAt githubv4.DateTime
Author struct {
Login string
}
}
} `graphql:"comments(first: 10)"`
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"discussions(first: $first, after: $after)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

// Define a type for the Discussions GraphQL query to avoid duplication
type discussionQueryType struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Number int
Title string
Body string
CreatedAt githubv4.DateTime
UpdatedAt githubv4.DateTime
URL githubv4.URI
Category struct {
Name string
}
Author struct {
Login string
}
Locked bool
UpvoteCount int
Comments struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Body string
CreatedAt githubv4.DateTime
Author struct {
Login string
}
}
} `graphql:"comments(first: 10)"`
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
}

// Add categoryId to query if it was provided
if categoryId != "" {
variables["categoryId"] = githubv4.ID(categoryId)
// Use a separate query structure that includes the categoryId parameter
var queryWithCategory struct {
Repository struct {
Discussions discussionQueryType `graphql:"discussions(first: $first, after: $after, categoryId: $categoryId)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

// Execute the query with categoryId
err = client.Query(ctx, &queryWithCategory, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions with category: %w", err)
}

// Copy the results to our main query structure
query.Repository.Discussions = queryWithCategory.Repository.Discussions
} else {
// Execute the original query without categoryId
err = client.Query(ctx, &query, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions: %w", err)
}
}

// Execute the GraphQL query
err = client.Query(ctx, &query, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions: %w", err)
}

// Convert the GraphQL response to our Discussion type
discussions := make([]Discussion, 0, len(query.Repository.Discussions.Nodes))
for _, node := range query.Repository.Discussions.Nodes {
// Process comments for this discussion
comments := make([]Comment, 0, len(node.Comments.Nodes))
for _, commentNode := range node.Comments.Nodes {
comment := Comment{
ID: fmt.Sprintf("%v", commentNode.ID),
Body: commentNode.Body,
CreatedAt: commentNode.CreatedAt.String(),
Author: commentNode.Author.Login,
}
comments = append(comments, comment)
}

discussion := Discussion{
ID: fmt.Sprintf("%v", node.ID),
Number: node.Number,
Title: node.Title,
Body: node.Body,
CreatedAt: node.CreatedAt.String(),
UpdatedAt: node.UpdatedAt.String(),
URL: node.URL.String(),
Category: node.Category.Name,
Author: node.Author.Login,
Locked: node.Locked,
UpvoteCount: node.UpvoteCount,
CommentCount: node.Comments.TotalCount,
Comments: comments,
}
discussions = append(discussions, discussion)
}

// Create the response
result := struct {
TotalCount int `json:"totalCount"`
Discussions []Discussion `json:"discussions"`
HasNextPage bool `json:"hasNextPage"`
EndCursor string `json:"endCursor"`
}{
TotalCount: query.Repository.Discussions.TotalCount,
Discussions: discussions,
HasNextPage: query.Repository.Discussions.PageInfo.HasNextPage,
EndCursor: string(query.Repository.Discussions.PageInfo.EndCursor),
}

// Marshal the result to JSON
r, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal discussions result: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}
8 changes: 7 additions & 1 deletionpkg/github/server.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,12 +12,14 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
)

type GetClientFn func(context.Context) (*github.Client, error)
type GetGraphQLClientFn func(context.Context) (*githubv4.Client, error)

// NewServer creates a new GitHub MCP server with the specified GH client and logger.
func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc, opts ...server.ServerOption) *server.MCPServer {
func NewServer(getClient GetClientFn,getGraphQLClient GetGraphQLClientFn,version string, readOnly bool, t translations.TranslationHelperFunc, opts ...server.ServerOption) *server.MCPServer {
// Add default options
defaultOpts := []server.ServerOption{
server.WithResourceCapabilities(true, true),
Expand DownExpand Up@@ -90,6 +92,10 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
// Add GitHub tools - Code Scanning
s.AddTool(GetCodeScanningAlert(getClient, t))
s.AddTool(ListCodeScanningAlerts(getClient, t))

// Add GitHub tools - Discussions (GraphQL)
s.AddTool(GetRepositoryDiscussions(getGraphQLClient, t))

return s
}

Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp