Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ The following sets of tools are available:
- `owner`: Optional repository owner. If provided with repo, only issues for this repository are listed. (string, optional)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `query`: Search query using GitHub issues search syntax (string, required)
- `query`: The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR. (string, required)
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)

Expand Down
2 changes: 1 addition & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ runtime behavior (such as output formatting) won't appear here.
- `owner`: Optional repository owner. If provided with repo, only issues for this repository are listed. (string, optional)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `query`: Search query using GitHub issues search syntax (string, required)
- `query`: The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR. (string, required)
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)

Expand Down
2 changes: 1 addition & 1 deletion docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
- `owner`: Optional repository owner. If provided with repo, only issues for this repository are listed. (string, optional)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `query`: Search query using GitHub issues search syntax (string, required)
- `query`: The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR. (string, required)
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.25.12
require (
github.com/go-chi/chi/v5 v5.3.1
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/google/go-github/v89 v89.0.0
github.com/google/go-github/v89 v89.0.1-0.20260728185857-34349a88bac3
github.com/google/jsonschema-go v0.4.3
github.com/josephburnett/jd/v2 v2.5.0
github.com/lithammer/fuzzysearch v1.1.8
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0=
github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho=
github.com/google/go-github/v89 v89.0.1-0.20260728185857-34349a88bac3 h1:0a/p9KtPso8UBauBD/p9Go1oaZrrEydBNHjaaKkSHJo=
github.com/google/go-github/v89 v89.0.1-0.20260728185857-34349a88bac3/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
Expand Down
7 changes: 6 additions & 1 deletion internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
return nil, fmt.Errorf("failed to parse API host: %w", err)
}

hostType, err := utils.ParseHostType(cfg.Host)
if err != nil {
return nil, fmt.Errorf("failed to classify API host: %w", err)
}

clients, err := createGitHubClients(cfg, apiHost)
if err != nil {
return nil, fmt.Errorf("failed to create GitHub clients: %w", err)
Expand Down Expand Up @@ -165,7 +170,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
obs,
)
// Build and register the tool/resource/prompt inventory
inventoryBuilder := github.NewInventory(cfg.Translator).
inventoryBuilder := github.NewInventory(cfg.Translator, github.WithHost(hostType)).
WithDeprecatedAliases(github.DeprecatedToolAliases).
WithReadOnly(cfg.ReadOnly).
WithToolsets(github.ResolvedEnabledToolsets(cfg.EnabledToolsets, cfg.EnabledTools)).
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/__toolsnaps__/search_issues.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"readOnlyHint": true,
"title": "Search issues"
},
"description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue",
"description": "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue.",
"inputSchema": {
"properties": {
"order": {
Expand All @@ -31,7 +31,7 @@
"type": "number"
},
"query": {
"description": "Search query using GitHub issues search syntax",
"description": "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR.",
"type": "string"
},
"repo": {
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/__toolsnaps__/search_issues_ff_fields_param.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"readOnlyHint": true,
"title": "Search issues"
},
"description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue",
"description": "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue.",
"inputSchema": {
"properties": {
"fields": {
Expand Down Expand Up @@ -64,7 +64,7 @@
"type": "number"
},
"query": {
"description": "Search query using GitHub issues search syntax",
"description": "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR.",
"type": "string"
},
"repo": {
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
// This function is stateless - no dependencies are captured.
// Handlers are generated on-demand during registration via RegisterAll(ctx, server, deps).
// The "default" keyword in WithToolsets will expand to toolsets marked with Default: true.
func NewInventory(t translations.TranslationHelperFunc) *inventory.Builder {
func NewInventory(t translations.TranslationHelperFunc, opts ...ToolOption) *inventory.Builder {
return inventory.NewBuilder().
SetTools(AllTools(t)).
SetTools(AllTools(t, opts...)).
SetResources(AllResources(t)).
SetPrompts(AllPrompts(t))
}
73 changes: 50 additions & 23 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -1601,8 +1601,8 @@ func ReprioritizeSubIssue(ctx context.Context, client *github.Client, owner stri
// LegacySearchIssues register under the tool name "search_issues"; exactly one is
// active for any given request thanks to mutually exclusive FeatureFlagEnable /
// FeatureFlagDisable annotations.
func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
st := searchIssuesTool(t, true)
func SearchIssues(t translations.TranslationHelperFunc, opts ...ToolOption) inventory.ServerTool {
st := searchIssuesTool(t, true, newToolConfig(opts))
st.FeatureFlagEnable = FeatureFlagFieldsParam
return st
}
Expand All @@ -1612,23 +1612,50 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
// filters results, so it acts as the kill switch when the flag is off. It owns
// the canonical search_issues.snap; the flag-enabled variant owns
// search_issues_ff_<flag>.snap. Delete this function when the flag is removed.
func LegacySearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
st := searchIssuesTool(t, false)
func LegacySearchIssues(t translations.TranslationHelperFunc, opts ...ToolOption) inventory.ServerTool {
st := searchIssuesTool(t, false, newToolConfig(opts))
st.FeatureFlagDisable = []string{FeatureFlagFieldsParam}
return st
}

// The two search engines want opposite things from a caller, so steering advice
// for one is counterproductive for the other: semantic rewards paraphrased
// natural language and degrades on boolean operators, while lexical needs the
// caller's literal keywords and handles OR fine. The description has to describe
// the engine the host will actually use.
const (
searchIssuesSemanticDescription = "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue."
searchIssuesLexicalDescription = "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue"

searchIssuesSemanticQueryDescription = "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR."
searchIssuesLexicalQueryDescription = "Search query using GitHub issues search syntax"
)

// searchIssuesTool builds the search_issues tool. When includeFields is true the
// tool advertises the optional `fields` parameter, filters each result to the
// requested subset, and emits fields telemetry. When false it is the original
// tool with no fields parameter and no filtering.
func searchIssuesTool(t translations.TranslationHelperFunc, includeFields bool) inventory.ServerTool {
func searchIssuesTool(t translations.TranslationHelperFunc, includeFields bool, cfg toolConfig) inventory.ServerTool {
// Semantic is the default; however as it is not available on GHES, we fall back to
// lexical search for that host type.
mode := searchModeSemantic
if cfg.hostType == utils.HostTypeGHES {
mode = searchModeLexical
}

toolDescription := searchIssuesSemanticDescription
queryDescription := searchIssuesSemanticQueryDescription
if mode == searchModeLexical {
toolDescription = searchIssuesLexicalDescription
queryDescription = searchIssuesLexicalQueryDescription
}

schema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"query": {
Type: "string",
Description: "Search query using GitHub issues search syntax",
Description: queryDescription,
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -1675,7 +1702,7 @@ func searchIssuesTool(t translations.TranslationHelperFunc, includeFields bool)
ToolsetMetadataIssues,
mcp.Tool{
Name: "search_issues",
Description: t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue"),
Description: t("TOOL_SEARCH_ISSUES_DESCRIPTION", toolDescription),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_ISSUES_USER_TITLE", "Search issues"),
ReadOnlyHint: true,
Expand All @@ -1692,7 +1719,7 @@ func searchIssuesTool(t translations.TranslationHelperFunc, includeFields bool)
}
options = append(options, withFieldsFiltering(deps, "search_issues", fields))
}
result, err := searchIssuesHandler(ctx, deps, args, options...)
result, err := searchIssuesHandler(ctx, deps, args, mode, options...)
return result, nil, err
})
}
Expand Down Expand Up @@ -1960,10 +1987,15 @@ func fetchIssueReadEnrichment(ctx context.Context, gqlClient *githubv4.Client, n
// searchIssuesHandler runs the REST issues search, enriches each hit with custom field values
// fetched via a single follow-up GraphQL nodes() query, and applies any post-process options
// (e.g. IFC labelling).
func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[string]any, options ...searchOption) (*mcp.CallToolResult, error) {
func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[string]any, mode searchMode, options ...searchOption) (*mcp.CallToolResult, error) {
const errorPrefix = "failed to search issues"

query, opts, err := prepareSearchArgs(args, "issue")
cfg := searchConfig{}
for _, opt := range options {
opt(&cfg)
}

query, opts, err := prepareSearchArgs(args, "issue", mode)
if err != nil {
return utils.NewToolResultError(err.Error()), nil
}
Expand Down Expand Up @@ -2013,11 +2045,6 @@ func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[st
Items: items,
}

cfg := searchConfig{}
for _, opt := range options {
opt(&cfg)
}

filtered := false
var payload any = response
if len(cfg.fields) > 0 {
Expand Down Expand Up @@ -2382,11 +2409,11 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo
}

// Create the issue request
issueRequest := &github.IssueRequest{
Title: github.Ptr(title),
issueRequest := github.CreateIssueRequest{
Title: title,
Body: github.Ptr(body),
Assignees: &assignees,
Labels: &labels,
Assignees: assignees,
Labels: labels,
IssueFieldValues: issueFieldValues,
}

Expand Down Expand Up @@ -2449,7 +2476,7 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
}

// Create the issue request with only provided fields
issueRequest := &github.IssueRequest{}
issueRequest := github.UpdateIssueRequest{}

// Set optional parameters if provided
if title != "" {
Expand All @@ -2461,11 +2488,11 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
}

if updateOptions.LabelsProvided {
issueRequest.Labels = &labels
issueRequest.Labels = labels
}

if updateOptions.AssigneesProvided {
issueRequest.Assignees = &assignees
issueRequest.Assignees = assignees
}

if milestoneNum != 0 {
Expand Down Expand Up @@ -2519,7 +2546,7 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
}
}

updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest)
updatedIssue, resp, err := client.Issues.Update(ctx, owner, repo, issueNumber, issueRequest)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to update issue",
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/issues_delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
func Test_IssueRequest_EmptyFieldValues_OmittedByJSON(t *testing.T) {
t.Parallel()

req := &gogithub.IssueRequest{
req := &gogithub.UpdateIssueRequest{
Title: gogithub.Ptr("still here"),
IssueFieldValues: []*gogithub.IssueRequestFieldValue{},
}
Expand Down
34 changes: 17 additions & 17 deletions pkg/github/issues_granular.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func issueUpdateTool(
name, description, title string,
extraProps map[string]*jsonschema.Schema,
extraRequired []string,
buildRequest func(args map[string]any) (*github.IssueRequest, error),
buildRequest func(args map[string]any) (github.UpdateIssueRequest, error),
) inventory.ServerTool {
props := map[string]*jsonschema.Schema{
"owner": {
Expand Down Expand Up @@ -92,7 +92,7 @@ func issueUpdateTool(
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

issue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueReq)
issue, resp, err := client.Issues.Update(ctx, owner, repo, issueNumber, issueReq)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update issue", resp, err), nil, nil
}
Expand Down Expand Up @@ -164,8 +164,8 @@ func GranularCreateIssue(t translations.TranslationHelperFunc) inventory.ServerT
}
body, _ := OptionalParam[string](args, "body")

issueReq := &github.IssueRequest{
Title: &title,
issueReq := github.CreateIssueRequest{
Title: title,
}
if body != "" {
issueReq.Body = &body
Expand Down Expand Up @@ -206,12 +206,12 @@ func GranularUpdateIssueTitle(t translations.TranslationHelperFunc) inventory.Se
"title": {Type: "string", Description: "The new title for the issue"},
},
[]string{"title"},
func(args map[string]any) (*github.IssueRequest, error) {
func(args map[string]any) (github.UpdateIssueRequest, error) {
title, err := RequiredParam[string](args, "title")
if err != nil {
return nil, err
return github.UpdateIssueRequest{}, err
}
return &github.IssueRequest{Title: &title}, nil
return github.UpdateIssueRequest{Title: &title}, nil
},
)
}
Expand All @@ -226,12 +226,12 @@ func GranularUpdateIssueBody(t translations.TranslationHelperFunc) inventory.Ser
"body": {Type: "string", Description: "The new body content for the issue"},
},
[]string{"body"},
func(args map[string]any) (*github.IssueRequest, error) {
func(args map[string]any) (github.UpdateIssueRequest, error) {
body, err := RequiredParam[string](args, "body")
if err != nil {
return nil, err
return github.UpdateIssueRequest{}, err
}
return &github.IssueRequest{Body: &body}, nil
return github.UpdateIssueRequest{Body: &body}, nil
},
)
}
Expand Down Expand Up @@ -392,7 +392,7 @@ func GranularUpdateIssueAssignees(t translations.TranslationHelperFunc) inventor
for i, p := range payload {
logins[i] = p.(string)
}
body = &github.IssueRequest{Assignees: &logins}
body = &github.UpdateIssueRequest{Assignees: logins}
}

apiURL := fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, issueNumber)
Expand Down Expand Up @@ -610,7 +610,7 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
for i, p := range payload {
names[i] = p.(string)
}
body = &github.IssueRequest{Labels: &names}
body = &github.UpdateIssueRequest{Labels: names}
}

apiURL := fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, issueNumber)
Expand Down Expand Up @@ -654,12 +654,12 @@ func GranularUpdateIssueMilestone(t translations.TranslationHelperFunc) inventor
},
},
[]string{"milestone"},
func(args map[string]any) (*github.IssueRequest, error) {
func(args map[string]any) (github.UpdateIssueRequest, error) {
milestone, err := RequiredInt(args, "milestone")
if err != nil {
return nil, err
return github.UpdateIssueRequest{}, err
}
return &github.IssueRequest{Milestone: &milestone}, nil
return github.UpdateIssueRequest{Milestone: &milestone}, nil
},
)
}
Expand Down Expand Up @@ -787,7 +787,7 @@ func GranularUpdateIssueType(t translations.TranslationHelperFunc) inventory.Ser
},
}
} else {
body = &github.IssueRequest{Type: &issueType}
body = &github.UpdateIssueRequest{Type: &issueType}
}

apiURL := fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, issueNumber)
Expand Down Expand Up @@ -981,7 +981,7 @@ func GranularUpdateIssueState(t translations.TranslationHelperFunc) inventory.Se
}
body = req
} else {
req := &github.IssueRequest{State: &state}
req := &github.UpdateIssueRequest{State: &state}
if stateReason != "" {
req.StateReason = &stateReason
}
Expand Down
Loading
Loading