Skip to content

Commit d42bb3e

Browse files
authored
Send update_issue_suggestions feature flag for set_issue_fields mutation (#2638)
* Using issues suggestions feature flag * Gate set_issue_fields confidence behind update_issue_confidence flag The GitHub GraphQL API does not yet accept the per-field confidence input on setIssueFieldValue mutations. Hide it from the user-facing schema and drop it from the mutation payload unless the new update_issue_confidence feature flag is enabled so users do not try to use it before the API supports it. * adding back confidence * Update set_issue_fields confidence schema and toolsnap
1 parent 457f599 commit d42bb3e

3 files changed

Lines changed: 197 additions & 3 deletions

File tree

pkg/github/__toolsnaps__/set_issue_fields.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"openWorldHint": true,
55
"title": "Set Issue Fields"
66
},
7-
"description": "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue. When setting values, include a confidence level (low, medium, or high) reflecting how certain you are about the choice.",
7+
"description": "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue.",
88
"inputSchema": {
99
"properties": {
1010
"fields": {

pkg/github/granular_tools_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
"github.com/github/github-mcp-server/internal/githubv4mock"
1010
"github.com/github/github-mcp-server/internal/toolsnaps"
11+
"github.com/github/github-mcp-server/pkg/http/headers"
12+
transportpkg "github.com/github/github-mcp-server/pkg/http/transport"
1113
"github.com/github/github-mcp-server/pkg/inventory"
1214
"github.com/github/github-mcp-server/pkg/translations"
1315
gogithub "github.com/google/go-github/v87/github"
@@ -1710,6 +1712,97 @@ func TestGranularSetIssueFields(t *testing.T) {
17101712
assert.Contains(t, textContent.Text, "confidence must be one of: low, medium, high")
17111713
})
17121714

1715+
t.Run("confidence is sent when supplied", func(t *testing.T) {
1716+
confidence := "high"
1717+
matchers := []githubv4mock.Matcher{
1718+
githubv4mock.NewQueryMatcher(
1719+
struct {
1720+
Repository struct {
1721+
Issue struct {
1722+
ID githubv4.ID
1723+
} `graphql:"issue(number: $issueNumber)"`
1724+
} `graphql:"repository(owner: $owner, name: $repo)"`
1725+
}{},
1726+
map[string]any{
1727+
"owner": githubv4.String("owner"),
1728+
"repo": githubv4.String("repo"),
1729+
"issueNumber": githubv4.Int(5),
1730+
},
1731+
githubv4mock.DataResponse(map[string]any{
1732+
"repository": map[string]any{
1733+
"issue": map[string]any{"id": "ISSUE_123"},
1734+
},
1735+
}),
1736+
),
1737+
githubv4mock.NewMutationMatcher(
1738+
struct {
1739+
SetIssueFieldValue struct {
1740+
Issue struct {
1741+
ID githubv4.ID
1742+
Number githubv4.Int
1743+
URL githubv4.String
1744+
}
1745+
IssueFieldValues []struct {
1746+
TextValue struct {
1747+
Value string
1748+
} `graphql:"... on IssueFieldTextValue"`
1749+
SingleSelectValue struct {
1750+
Name string
1751+
} `graphql:"... on IssueFieldSingleSelectValue"`
1752+
DateValue struct {
1753+
Value string
1754+
} `graphql:"... on IssueFieldDateValue"`
1755+
NumberValue struct {
1756+
Value float64
1757+
} `graphql:"... on IssueFieldNumberValue"`
1758+
}
1759+
} `graphql:"setIssueFieldValue(input: $input)"`
1760+
}{},
1761+
SetIssueFieldValueInput{
1762+
IssueID: githubv4.ID("ISSUE_123"),
1763+
IssueFields: []IssueFieldCreateOrUpdateInput{
1764+
{
1765+
FieldID: githubv4.ID("FIELD_1"),
1766+
TextValue: githubv4.NewString(githubv4.String("hello")),
1767+
Confidence: &confidence,
1768+
},
1769+
},
1770+
},
1771+
nil,
1772+
githubv4mock.DataResponse(map[string]any{
1773+
"setIssueFieldValue": map[string]any{
1774+
"issue": map[string]any{
1775+
"id": "ISSUE_123",
1776+
"number": 5,
1777+
"url": "https://github.com/owner/repo/issues/5",
1778+
},
1779+
},
1780+
}),
1781+
),
1782+
}
1783+
1784+
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
1785+
deps := BaseDeps{GQLClient: gqlClient}
1786+
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
1787+
handler := serverTool.Handler(deps)
1788+
1789+
request := createMCPRequest(map[string]any{
1790+
"owner": "owner",
1791+
"repo": "repo",
1792+
"issue_number": float64(5),
1793+
"fields": []any{
1794+
map[string]any{
1795+
"field_id": "FIELD_1",
1796+
"text_value": "hello",
1797+
"confidence": "high",
1798+
},
1799+
},
1800+
})
1801+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1802+
require.NoError(t, err)
1803+
assert.False(t, result.IsError, getTextResult(t, result).Text)
1804+
})
1805+
17131806
t.Run("successful set with suggest flag", func(t *testing.T) {
17141807
suggestTrue := githubv4.Boolean(true)
17151808
matchers := []githubv4mock.Matcher{
@@ -1802,4 +1895,101 @@ func TestGranularSetIssueFields(t *testing.T) {
18021895
require.NoError(t, err)
18031896
assert.False(t, result.IsError)
18041897
})
1898+
1899+
t.Run("sends GraphQL-Features: update_issue_suggestions header on mutation", func(t *testing.T) {
1900+
matchers := []githubv4mock.Matcher{
1901+
githubv4mock.NewQueryMatcher(
1902+
struct {
1903+
Repository struct {
1904+
Issue struct {
1905+
ID githubv4.ID
1906+
} `graphql:"issue(number: $issueNumber)"`
1907+
} `graphql:"repository(owner: $owner, name: $repo)"`
1908+
}{},
1909+
map[string]any{
1910+
"owner": githubv4.String("owner"),
1911+
"repo": githubv4.String("repo"),
1912+
"issueNumber": githubv4.Int(5),
1913+
},
1914+
githubv4mock.DataResponse(map[string]any{
1915+
"repository": map[string]any{
1916+
"issue": map[string]any{"id": "ISSUE_123"},
1917+
},
1918+
}),
1919+
),
1920+
githubv4mock.NewMutationMatcher(
1921+
struct {
1922+
SetIssueFieldValue struct {
1923+
Issue struct {
1924+
ID githubv4.ID
1925+
Number githubv4.Int
1926+
URL githubv4.String
1927+
}
1928+
IssueFieldValues []struct {
1929+
TextValue struct {
1930+
Value string
1931+
} `graphql:"... on IssueFieldTextValue"`
1932+
SingleSelectValue struct {
1933+
Name string
1934+
} `graphql:"... on IssueFieldSingleSelectValue"`
1935+
DateValue struct {
1936+
Value string
1937+
} `graphql:"... on IssueFieldDateValue"`
1938+
NumberValue struct {
1939+
Value float64
1940+
} `graphql:"... on IssueFieldNumberValue"`
1941+
}
1942+
} `graphql:"setIssueFieldValue(input: $input)"`
1943+
}{},
1944+
SetIssueFieldValueInput{
1945+
IssueID: githubv4.ID("ISSUE_123"),
1946+
IssueFields: []IssueFieldCreateOrUpdateInput{
1947+
{
1948+
FieldID: githubv4.ID("FIELD_1"),
1949+
TextValue: githubv4.NewString(githubv4.String("hello")),
1950+
},
1951+
},
1952+
},
1953+
nil,
1954+
githubv4mock.DataResponse(map[string]any{
1955+
"setIssueFieldValue": map[string]any{
1956+
"issue": map[string]any{
1957+
"id": "ISSUE_123",
1958+
"number": 5,
1959+
"url": "https://github.com/owner/repo/issues/5",
1960+
},
1961+
},
1962+
}),
1963+
),
1964+
}
1965+
1966+
// Build a transport chain matching production: GraphQLFeaturesTransport
1967+
// wraps a header-capturing spy, which forwards to the mock's RoundTripper.
1968+
// This verifies the mutation request sets the update_issue_suggestions
1969+
// feature flag so the rationale/suggest input fields are accepted.
1970+
mockClient := githubv4mock.NewMockedHTTPClient(matchers...)
1971+
spy := &headerCaptureTransport{inner: mockClient.Transport}
1972+
httpClient := &http.Client{
1973+
Transport: &transportpkg.GraphQLFeaturesTransport{Transport: spy},
1974+
}
1975+
gqlClient := githubv4.NewClient(httpClient)
1976+
deps := BaseDeps{GQLClient: gqlClient}
1977+
serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
1978+
handler := serverTool.Handler(deps)
1979+
1980+
request := createMCPRequest(map[string]any{
1981+
"owner": "owner",
1982+
"repo": "repo",
1983+
"issue_number": float64(5),
1984+
"fields": []any{
1985+
map[string]any{"field_id": "FIELD_1", "text_value": "hello"},
1986+
},
1987+
})
1988+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1989+
require.NoError(t, err)
1990+
require.False(t, result.IsError, getTextResult(t, result).Text)
1991+
// The last request captured is the mutation; the preceding issue ID
1992+
// query does not require the feature flag.
1993+
assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
1994+
})
18051995
}

pkg/github/issues_granular.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"maps"
88
"strings"
99

10+
ghcontext "github.com/github/github-mcp-server/pkg/context"
1011
ghErrors "github.com/github/github-mcp-server/pkg/errors"
1112
"github.com/github/github-mcp-server/pkg/inventory"
1213
"github.com/github/github-mcp-server/pkg/scopes"
@@ -924,7 +925,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
924925
ToolsetMetadataIssues,
925926
mcp.Tool{
926927
Name: "set_issue_fields",
927-
Description: t("TOOL_SET_ISSUE_FIELDS_DESCRIPTION", "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue. When setting values, include a confidence level (low, medium, or high) reflecting how certain you are about the choice."),
928+
Description: t("TOOL_SET_ISSUE_FIELDS_DESCRIPTION", "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue."),
928929
Annotations: &mcp.ToolAnnotations{
929930
Title: t("TOOL_SET_ISSUE_FIELDS_USER_TITLE", "Set Issue Fields"),
930931
ReadOnlyHint: false,
@@ -1170,7 +1171,10 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
11701171
IssueFields: issueFields,
11711172
}
11721173

1173-
if err := gqlClient.Mutate(ctx, &mutation, mutationInput, nil); err != nil {
1174+
// The rationale and suggest input fields on IssueFieldCreateOrUpdateInput
1175+
// are gated behind the update_issue_suggestions GraphQL feature flag.
1176+
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "update_issue_suggestions")
1177+
if err := gqlClient.Mutate(ctxWithFeatures, &mutation, mutationInput, nil); err != nil {
11741178
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil
11751179
}
11761180

0 commit comments

Comments
 (0)