chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
ghcontext "github.com/github/github-mcp-server/pkg/context"
|
||||
ghErrors "github.com/github/github-mcp-server/pkg/errors"
|
||||
"github.com/github/github-mcp-server/pkg/ifc"
|
||||
"github.com/github/github-mcp-server/pkg/inventory"
|
||||
"github.com/github/github-mcp-server/pkg/scopes"
|
||||
"github.com/github/github-mcp-server/pkg/translations"
|
||||
"github.com/github/github-mcp-server/pkg/utils"
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
// IssueField represents a repository issue field definition.
|
||||
type IssueField struct {
|
||||
ID string `json:"id"`
|
||||
DatabaseID int64 `json:"full_database_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DataType string `json:"data_type"`
|
||||
Visibility string `json:"visibility"`
|
||||
Options []IssueSingleSelectFieldOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// IssueSingleSelectFieldOption represents an option for a single_select issue field.
|
||||
type IssueSingleSelectFieldOption struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Color string `json:"color"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
// issueFieldNode is the GraphQL fragment for a single issue field in the IssueFields union.
|
||||
// Only the fragment matching __typename is populated; read from the matching fragment.
|
||||
// fullDatabaseId (BigInt scalar, returned as string) is fetched on each concrete type because
|
||||
// shurcooL/githubv4 does not support interface fragments at the top level of a union.
|
||||
type issueFieldNode struct {
|
||||
TypeName githubv4.String `graphql:"__typename"`
|
||||
IssueFieldText struct {
|
||||
ID githubv4.ID
|
||||
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
|
||||
Name githubv4.String
|
||||
Description githubv4.String
|
||||
DataType githubv4.String
|
||||
Visibility githubv4.String
|
||||
} `graphql:"... on IssueFieldText"`
|
||||
IssueFieldNumber struct {
|
||||
ID githubv4.ID
|
||||
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
|
||||
Name githubv4.String
|
||||
Description githubv4.String
|
||||
DataType githubv4.String
|
||||
Visibility githubv4.String
|
||||
} `graphql:"... on IssueFieldNumber"`
|
||||
IssueFieldDate struct {
|
||||
ID githubv4.ID
|
||||
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
|
||||
Name githubv4.String
|
||||
Description githubv4.String
|
||||
DataType githubv4.String
|
||||
Visibility githubv4.String
|
||||
} `graphql:"... on IssueFieldDate"`
|
||||
IssueFieldSingleSelect struct {
|
||||
ID githubv4.ID
|
||||
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
|
||||
Name githubv4.String
|
||||
Description githubv4.String
|
||||
DataType githubv4.String
|
||||
Visibility githubv4.String
|
||||
Options []struct {
|
||||
ID githubv4.ID
|
||||
Name githubv4.String
|
||||
Description githubv4.String
|
||||
Color githubv4.String
|
||||
Priority *int
|
||||
}
|
||||
} `graphql:"... on IssueFieldSingleSelect"`
|
||||
}
|
||||
|
||||
// issueFieldsRepoQuery is the GraphQL query for listing issue fields on a repository.
|
||||
type issueFieldsRepoQuery struct {
|
||||
Repository struct {
|
||||
IssueFields struct {
|
||||
Nodes []issueFieldNode
|
||||
} `graphql:"issueFields(first: 100)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
// issueFieldsOrgQuery is the GraphQL query for listing issue fields on an organization.
|
||||
type issueFieldsOrgQuery struct {
|
||||
Organization struct {
|
||||
IssueFields struct {
|
||||
Nodes []issueFieldNode
|
||||
} `graphql:"issueFields(first: 100)"`
|
||||
} `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
// ListIssueFields creates a tool to list issue field definitions for a repository or organization.
|
||||
func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool {
|
||||
st := NewTool(
|
||||
ToolsetMetadataIssues,
|
||||
mcp.Tool{
|
||||
Name: "list_issue_fields",
|
||||
Description: t("TOOL_LIST_ISSUE_FIELDS_DESCRIPTION", "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select), and for single_select fields the list of valid option names. When repo is omitted, returns org-level fields directly."),
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: t("TOOL_LIST_ISSUE_FIELDS_USER_TITLE", "List issue fields"),
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"owner": {
|
||||
Type: "string",
|
||||
Description: "The account owner of the repository or organization. The name is not case sensitive.",
|
||||
},
|
||||
"repo": {
|
||||
Type: "string",
|
||||
Description: "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
|
||||
},
|
||||
},
|
||||
Required: []string{"owner"},
|
||||
},
|
||||
},
|
||||
[]scopes.Scope{scopes.Repo, scopes.ReadOrg},
|
||||
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
||||
owner, err := RequiredParam[string](args, "owner")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
repo, err := OptionalParam[string](args, "repo")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
|
||||
gqlClient, err := deps.GetGQLClient(ctx)
|
||||
if err != nil {
|
||||
return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
|
||||
}
|
||||
|
||||
fields, err := fetchIssueFields(ctx, gqlClient, owner, repo)
|
||||
if err != nil {
|
||||
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(fields)
|
||||
if err != nil {
|
||||
return utils.NewToolResultErrorFromErr("failed to marshal issue fields", err), nil, nil
|
||||
}
|
||||
|
||||
result := utils.NewToolResultText(string(r))
|
||||
// Issue field definitions are repo/org structural metadata
|
||||
// (trusted). When scoped to a specific repo, confidentiality
|
||||
// follows that repo's visibility; for an org-level lookup (no
|
||||
// repo) it is conservatively treated as private.
|
||||
if repo == "" {
|
||||
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelRepoMetadata(true))
|
||||
} else {
|
||||
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoMetadata)
|
||||
}
|
||||
return result, nil, nil
|
||||
})
|
||||
return st
|
||||
}
|
||||
|
||||
// fetchIssueFields returns the issue field definitions for the given owner.
|
||||
// If repo is provided, fields are scoped to that repository (inherited from its
|
||||
// organization); otherwise fields are returned directly from the organization.
|
||||
func fetchIssueFields(ctx context.Context, gqlClient *githubv4.Client, owner, repo string) ([]IssueField, error) {
|
||||
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
|
||||
if repo != "" {
|
||||
var query issueFieldsRepoQuery
|
||||
vars := map[string]any{
|
||||
"owner": githubv4.String(owner),
|
||||
"name": githubv4.String(repo),
|
||||
}
|
||||
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return issueFieldsFromNodes(query.Repository.IssueFields.Nodes), nil
|
||||
}
|
||||
|
||||
var query issueFieldsOrgQuery
|
||||
vars := map[string]any{
|
||||
"login": githubv4.String(owner),
|
||||
}
|
||||
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return issueFieldsFromNodes(query.Organization.IssueFields.Nodes), nil
|
||||
}
|
||||
|
||||
// issueFieldsFromNodes converts GraphQL issue field union nodes into IssueField values.
|
||||
// Read from the fragment matching __typename; the other fragments are zero-valued.
|
||||
func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
|
||||
fields := make([]IssueField, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
var f IssueField
|
||||
switch string(node.TypeName) {
|
||||
case "IssueFieldSingleSelect":
|
||||
opts := make([]IssueSingleSelectFieldOption, 0, len(node.IssueFieldSingleSelect.Options))
|
||||
for _, o := range node.IssueFieldSingleSelect.Options {
|
||||
opts = append(opts, IssueSingleSelectFieldOption{
|
||||
ID: fmt.Sprintf("%v", o.ID),
|
||||
Name: string(o.Name),
|
||||
Description: string(o.Description),
|
||||
Color: string(o.Color),
|
||||
Priority: o.Priority,
|
||||
})
|
||||
}
|
||||
f = IssueField{
|
||||
ID: fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
|
||||
DatabaseID: parseFullDatabaseID(string(node.IssueFieldSingleSelect.FullDatabaseID)),
|
||||
Name: string(node.IssueFieldSingleSelect.Name),
|
||||
Description: string(node.IssueFieldSingleSelect.Description),
|
||||
DataType: string(node.IssueFieldSingleSelect.DataType),
|
||||
Visibility: string(node.IssueFieldSingleSelect.Visibility),
|
||||
Options: opts,
|
||||
}
|
||||
case "IssueFieldText":
|
||||
f = IssueField{
|
||||
ID: fmt.Sprintf("%v", node.IssueFieldText.ID),
|
||||
DatabaseID: parseFullDatabaseID(string(node.IssueFieldText.FullDatabaseID)),
|
||||
Name: string(node.IssueFieldText.Name),
|
||||
Description: string(node.IssueFieldText.Description),
|
||||
DataType: string(node.IssueFieldText.DataType),
|
||||
Visibility: string(node.IssueFieldText.Visibility),
|
||||
}
|
||||
case "IssueFieldNumber":
|
||||
f = IssueField{
|
||||
ID: fmt.Sprintf("%v", node.IssueFieldNumber.ID),
|
||||
DatabaseID: parseFullDatabaseID(string(node.IssueFieldNumber.FullDatabaseID)),
|
||||
Name: string(node.IssueFieldNumber.Name),
|
||||
Description: string(node.IssueFieldNumber.Description),
|
||||
DataType: string(node.IssueFieldNumber.DataType),
|
||||
Visibility: string(node.IssueFieldNumber.Visibility),
|
||||
}
|
||||
case "IssueFieldDate":
|
||||
f = IssueField{
|
||||
ID: fmt.Sprintf("%v", node.IssueFieldDate.ID),
|
||||
DatabaseID: parseFullDatabaseID(string(node.IssueFieldDate.FullDatabaseID)),
|
||||
Name: string(node.IssueFieldDate.Name),
|
||||
Description: string(node.IssueFieldDate.Description),
|
||||
DataType: string(node.IssueFieldDate.DataType),
|
||||
Visibility: string(node.IssueFieldDate.Visibility),
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
fields = append(fields, f)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// parseFullDatabaseID converts a BigInt scalar string (e.g. "12345") to int64.
|
||||
// Returns 0 if the string is empty or cannot be parsed.
|
||||
func parseFullDatabaseID(s string) int64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user