- Expose fullDatabaseId (BigInt) in list_issue_fields
- Add issue_fields parameter to issue_write for setting field values
- Support single-select fields via field_option_name resolution
- Add REST API field value extraction in get_issue responses
- Update minimal types with IssueFieldValue for REST responses
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
X-Forwarded-Host and X-Forwarded-Proto were unconditionally honored when
constructing OAuth resource metadata URLs. In HTTP-mode deployments that
do not set --base-url and are not fronted by a proxy that strips these
headers, this lets an on-path client influence the URL advertised in
WWW-Authenticate and the /.well-known/oauth-protected-resource body.
This is a hardening change rather than a true vulnerability — exploiting
it requires HTTP without --base-url plus an attacker already positioned
to inject the header — but the unsafe default is worth closing.
Default behavior now derives host/scheme from r.Host and the TLS state.
Setups that rely on a trusted internal forwarder (e.g. an in-cluster
gateway that needs to preserve the originating hostname per request) can
opt back in with --trust-proxy-headers / GITHUB_TRUST_PROXY_HEADERS=1.
--base-url continues to take precedence in all cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two auto-generated documentation sections that describe how
feature flags shape the tool surface:
- docs/insiders-features.md gets a per-flag block under its existing
hand-written prose. Each Insiders flag whose tools differ from the
default surface is listed with the full tool schema rendered through
the same writer used for README, so contributors can see exactly what
Insiders Mode adds or changes.
- docs/feature-flags.md is new and gives the same treatment to every
flag in AllowedFeatureFlags (user-controllable flags). It links back
to the Insiders doc for the auto-enabled subset.
Both sections are produced by a single generator that diffs the
flag-on inventory against the default-flagged inventory and reports any
tool that is new or has a different InputSchema/Meta. No reason
classification - just tools and their schemas, kept intentionally
simple so contributors don't have to update the generator when adding
a new flag.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(issues): gate issue-fields features behind remote_mcp_issue_fields flag
Gates the recently merged issue-fields work (list_issue_fields tool,
field_values enrichment on list_issues/search_issues, and field_filters
input on list_issues) behind a new feature flag, also enabled in
insiders mode.
- list_issues splits into two same-named registrations: the field-aware
variant requires the flag, while LegacyListIssues (FeatureFlagDisable)
preserves the prior schema and GraphQL selection set so disabled
callers don't pay the extra wire/server cost.
- search_issues skips the field-values lookup when the flag is off.
- list_issue_fields requires the flag to be registered at all.
- Adopts <tool>_ff_<flag>.snap naming for flagged toolsnap variants so
same-named duplicates each get a distinct snapshot.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address PR review on issue-fields gating
- docs generator: install a no-flags feature checker so README reflects
the default user experience (tools enabled with no special flags),
fixing duplicate `list_issues` and removing granular/flagged-only
tools that were never meant to appear in the default docs.
- csv_output: drop the FeatureFlagEnable/Disable exclusion in
isCSVOutputTool. Wrapping happens before the per-request flag filter
picks the live variant, so flag-gated list_* tools wrap safely; this
restores CSV conversion for `list_issues` and enables it for
`list_issue_fields` when both flags are on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add CSV output for list tools under insiders mode
* fix: resolve rebase feature flag conflicts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify feature-flag handling: collapse CSV dual-variant + skip filtering when no checker (#2516)
* refactor: generic toolset+name sort, clarify feature flag intent
Address review feedback on #2450:
- Collapse the three near-identical sort helpers in pkg/inventory/filters.go
into a generic sortByToolsetThenName so adding new inventory item types
doesn't require copying the comparator.
- Expand the doc comments on the three *WithoutFeatureFiltering helpers to
spell out why they exist: HTTP mode builds a static (process-wide)
inventory as an upper bound, but per-request feature flags from headers
(X-MCP-Features, X-MCP-Insiders) are evaluated later, so feature-flagged
variants must be preserved here.
- Strengthen the doc comment on ResolveFeatureFlags to make the contract
explicit: user-supplied flags are validated against AllowedFeatureFlags,
but insiders expansion deliberately is not — InsidersFeatureFlags may
include server-controlled flags that are not user-toggleable.
CORS comments are intentionally left for the PR author.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(feature-flags): clarify allowed and insiders sets are independent
Also add tests covering:
- a user-toggleable flag (FeatureFlagIssuesGranular) that insiders does
not turn on automatically
- insiders mode not turning on user-only allowed flags
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(inventory): collapse three *WithoutFeatureFiltering helpers into StaticUpperBound
The three parallel methods (AvailableToolsWithoutFeatureFiltering,
AvailableResourceTemplatesWithoutFeatureFiltering,
AvailablePromptsWithoutFeatureFiltering) were always called as a triple
in exactly two places: HTTP buildStaticInventory and its test mirror.
They exist because the dual-variant pattern (sibling tools with mirrored
FeatureFlagEnable / FeatureFlagDisable on the same name, e.g. CSV output)
makes feature filtering at static-build time impossible — both variants
must be kept and resolved per-request.
Replace the three with one method, Inventory.StaticUpperBound(ctx), that
returns (tools, resources, prompts) and carries the rationale in its
doc comment. Reduces API surface, eliminates the triplication, and makes
the single "skip feature filtering" concept obvious to readers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: simplify feature-flag handling
Two related simplifications, both about treating insiders as a meta flag
that expands once at startup and then stops mattering:
- Collapse CSV's dual-variant pattern into a single tool whose handler
performs a runtime feature-flag check via deps.IsFeatureEnabled. CSV
is a pure response-format toggle, not a schema change, so it does not
need the dual-name pattern that genuine schema variants (granular
issues/PRs) still use.
- When no feature checker is installed, skip feature-flag filtering and
return the full upper bound. The static HTTP inventory now uses plain
AvailableTools/Resources/Prompts; the per-request inventory always
installs a checker, so MCP registration (which serves a tool name once)
always sees a deduplicated set. The bespoke StaticUpperBound helper and
the isToolEnabledWithFeatureFlags split go away.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ci(mcp-diff): add insiders + per-feature configs
The mcp-diff matrix now includes:
- --insiders (and --insiders --read-only)
- one config per github.AllowedFeatureFlags entry, generated by
script/print-mcp-diff-configs so new user-controllable flags get
diffed automatically without editing the workflow
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(insiders): explain feature-flag resolution for contributors
Adds a 'How feature flags are resolved' section covering:
- Insiders is a meta flag, like 'all'/'default' for toolsets
- User input -> allowlist filter -> insiders expansion ->
server-side fallback (remote only)
- AllowedFeatureFlags vs InsidersFeatureFlags are independent
- How to add a new feature flag, including the
TestGitHubPackageDoesNotReadInsidersMode guard
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(inventory): make feature-flag gating a regular ToolFilter
Move tool feature-flag evaluation out of isToolEnabled and into a
ToolFilter installed at the head of the pipeline by Build() when
WithFeatureChecker received a non-nil checker. The 'no checker = no
filtering' contract is now expressed structurally (the filter isn't
installed) instead of by a runtime nil check inside the helper.
Resources and prompts have no filter pipeline, so they call the now-pure
featureFlagAllowed helper behind an explicit r.featureChecker != nil
guard at the iteration site.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf(inventory): cache extracted toolset IDs in sort comparator
Avoid evaluating the extractor closures up to three times per comparison.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: correct MCP features header in cors
* docs: regenerate README for CSV output toolset
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove duplicate MCPFeaturesHeader from CORS headers
* ci(mcp-diff): add streamable-http job with header-based configs
Adds a sibling mcp-diff-http job that exercises the streamable-http
transport against a shared HTTP server, with per-config settings supplied
via X-MCP-* request headers — mirroring how the remote server is invoked
in production (server-side defaults + per-user header overrides).
The config generator gains a -transport flag:
- stdio (default, unchanged behaviour)
- http-headers (emits headers-only configs targeting a shared server)
Two new combined entries layer multiple headers together as a smoke test
for header-merging regressions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: regenerate after merging main
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Sam Morrow <info@sam-morrow.com>
Co-authored-by: sammorrowdrums <sammorrowdrums@github.com>
* Add custom field filtering to list_issues
* Flatten schema
* add repo fields flag
* test fix
---------
Co-authored-by: Sam Morrow <sammorrowdrums@github.com>
* Add list_org_issue_fields tool
* Clean up code
* complete struct fields & rename option type
* Drop created_at/updated_at from IssueField and IssueSingleSelectFieldOption
* Address feedback
* Address Copilot review: close resp.Body, set expectError=true for missing org test
* Adjust to list_issue_fields
* Add feature flag
* Allow tool to support read:org or repo
* Docs
* address comments
* Add repo_issue_fields flag
---------
Co-authored-by: Michael Jacholke <46944669+michaeljacholke@users.noreply.github.com>
* add `SearchCommits` tool
* run test
* run script/generate-docs
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor(search_commits): share commit conversion, surface repo, tighten query docs
- Extract newMinimalCommitFromCore to share field mapping between
convertToMinimalCommit (RepositoryCommit) and the new
convertCommitResultToMinimalCommit (CommitResult), removing ~50
lines of duplicated logic from the search_commits handler.
- Add MinimalRepoRef and a search-only MinimalCommitSearchItem type
(embedding MinimalCommit) so cross-repo commit search results
identify the repo each commit came from. Keeping the field off
MinimalCommit avoids paying for a never-populated field on the
get_commit/list_commits output types.
- Rewrite the query description to teach the model the actual
commit-search qualifier surface (repo:/org:/user: scoping, author/
committer/date qualifiers, hash/tree/parent, merge:, is:public)
and reword the sort description to drop redundancy with the enum.
- Extend tests to assert the repository field is surfaced and to
cover commits with no resolved GitHub user (nil Author/Committer).
- Refresh README and toolsnap.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sam Morrow <info@sam-morrow.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The current `search_code` query description is hand-wavy and gives the
model little usable guidance on GitHub code search syntax, which (per
analysis in #2390 across thousands of agent sessions) leads to repeated
422 ERROR_TYPE_QUERY_PARSING_FATAL responses from agents that guess at
plausible-but-invalid syntax.
Re-applies the spirit of #2442 by @jluocsa, originally suggested by
@danmoseley in #2390, but corrected against the actual endpoint this
tool calls.
Critically, this tool uses go-github's `client.Search.Code`, which hits
the legacy REST `/search/code` endpoint — NOT the new code search
("Blackbird"). Verified against the live API:
symbol:WithContext repo:github/github-mcp-server -> 0
/Get|Set/ repo:github/github-mcp-server -> 0
path:**/*.go func repo:github/github-mcp-server -> 0
filename:*.md repo:github/github-mcp-server -> 0
(Foo OR Bar) -path:vendor language:go -> 422
So `symbol:`, `/regex/`, path globs, filename globs, and parenthesized
boolean groups — features the proposal in #2442 listed — silently
return zero or fail. Documenting them would teach the model syntax
that doesn't work on this endpoint.
The new description focuses on what's actually supported by legacy
code search and the real bugs observed in #2390:
- `path:dir` is a prefix, NOT a glob (displaces `path:**/*.ts` guesses).
- `filename:exact.ext` is exact, NOT a glob (displaces `filename:*.md`).
- `/regex/` and `\|` inside quotes don't work — call this out so the
model stops generating them.
- `symbol:` doesn't work on this endpoint — call this out.
- Parenthesized boolean groups 422 — call this out so the model
stops wrapping `OR` chains in parens.
- Adds `extension:`, `in:file`, `in:path`, `size:`, `filename:`, `user:`
qualifiers that the previous text omitted.
- Implicit AND, `OR`, `NOT`, and `"quoted phrase"` for exact match are
documented positively.
- 256-char query limit.
All four examples in the new description are verified against the live
GitHub API and return non-zero results.
Co-authored-by: jluocsa <103165870+jluocsa@users.noreply.github.com>
Co-authored-by: danmoseley <6385855+danmoseley@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Include custom issue field values in list_issues response
Adds Issues 2.0 custom field values to each issue returned by the
list_issues GraphQL query, exposed on MinimalIssue as field_values:
[{field, value}]. Filtering by field is a separate concern (needs the
GraphQL IssueFilters input updated upstream) and is not included here.
shurcooL/graphql's response decoder walks every inline fragment of a
union regardless of __typename, so IssueFieldNumberValue.value is
aliased to valueNumber to avoid a Float-vs-String type clash when the
runtime variant is, e.g., a SingleSelectValue.
* Extend list_issues tests to cover Date/Number/Text field value variants
---------
Co-authored-by: Sam Morrow <info@sam-morrow.com>
Dynamic toolset discovery (the meta-tools enable_toolset, list_available_toolsets,
get_toolset_tools and the --dynamic-toolsets / GITHUB_DYNAMIC_TOOLSETS switch)
was a local-only feature never offered by the remote server. Removing it
deletes a meaningful chunk of branching, configuration surface and tests
for a path no longer in active use.
The deprecated closure-based NewServerToolWithDeps generic constructor was
only kept around for the dynamic tool registration path and is removed
together with it. Going forward there are exactly two constructors:
- NewServerTool — raw mcp.ToolHandler, no closure, no unmarshalling
- NewServerToolWithContextHandler[In, Out] — typed handler, deps via context
Inventory methods that only existed for the dynamic path
(ToolsForToolset, IsToolsetEnabled, EnableToolset, EnabledToolsetIDs)
are removed. ResolvedEnabledToolsets loses its dynamic flag.
Also strips dynamic references from the README, server configuration
docs, copilot-instructions, mcp-diff workflow, and conformance-test
script.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: simplify NewServerTool naming
- Rename NewServerToolWithRawContextHandler -> NewServerTool. This is the
preferred constructor for raw mcp.ToolHandler tools because it avoids
creating closures at registration time, which matters for per-request
servers that re-register all tools on every request.
- Rename deprecated generic NewServerTool[In, Out] -> NewServerToolWithDeps
to free up the simpler name and make its closure-based nature explicit.
The dynamic tools package is the only legitimate user of this constructor
because DynamicToolDependencies differs from the standard ToolDependencies.
- Remove deprecated NewServerToolFromHandler. Its only callers can use the
new NewServerTool directly via context-injected deps.
- Update all call sites in dependencies.go, dynamic_tools.go, and
registry_test.go.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: return isError for argument validation failures
When tool argument unmarshalling fails (wrong types, malformed JSON),
return a CallToolResult with IsError: true instead of a Go error.
Returning a Go error is converted by the SDK into a JSON-RPC protocol
error (-32603), which is invisible to agents and prevents self-correction.
Returning IsError: true with the validation message lets agents see the
problem and retry with corrected arguments.
Affects:
- NewServerToolWithDeps (was NewServerTool prior to the rename in #2510)
- NewServerToolWithContextHandler
Fixes#1952.
Re-applies #2488 by @blackwell-systems on top of the NewServerTool rename.
Co-authored-by: blackwell-systems <236632453+blackwell-systems@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: blackwell-systems <236632453+blackwell-systems@users.noreply.github.com>
- Rename NewServerToolWithRawContextHandler -> NewServerTool. This is the
preferred constructor for raw mcp.ToolHandler tools because it avoids
creating closures at registration time, which matters for per-request
servers that re-register all tools on every request.
- Rename deprecated generic NewServerTool[In, Out] -> NewServerToolWithDeps
to free up the simpler name and make its closure-based nature explicit.
The dynamic tools package is the only legitimate user of this constructor
because DynamicToolDependencies differs from the standard ToolDependencies.
- Remove deprecated NewServerToolFromHandler. Its only callers can use the
new NewServerTool directly via context-injected deps.
- Update all call sites in dependencies.go, dynamic_tools.go, and
registry_test.go.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The `pull_request_read` tool description tells clients that
`get_review_comments` uses cursor-based pagination (`perPage`, `after`),
and the handler does plumb `after` through to the GraphQL query, but the
input schema only declared `page` and `perPage` (via `WithPagination`).
Because `after` was not advertised in `inputSchema`, MCP clients had
no way to request it, leaving cursor pagination effectively broken:
`perPage: 1` returned only the first thread with no way to advance, and
`page` was silently ignored by the GraphQL path.
This change adds `after` to the schema (string, optional) with a
description making clear it only applies to `get_review_comments`. All
other methods continue to ignore it. No handler behavior is changed.
- Add `after` schema property after `WithPagination` in `PullRequestRead`
- Regenerate `__toolsnaps__/pull_request_read.snap` and update README
- Add a regression test asserting `after` is in the schema and a new
table-driven case verifying the cursor is forwarded to the GraphQL query
Fixes#2122 (for the `get_review_comments` pagination part). The
remaining concerns in #2122 about unbounded response sizes for `get`,
`get_diff`, and `get_reviews` are deferred to follow-up design.
Co-authored-by: Sam Morrow <info@sam-morrow.com>
Breaking changes addressed:
- raw.NewClient: Use WithHTTPClient/WithEnterpriseURLs options, pass ctx to
NewRequest, return (*Client, error)
- internal/ghmcp/server.go: Use functional options for REST client creation,
replace UserAgent field mutation with UserAgentTransport wrapper, add
restUATransp field to githubClients struct
- pkg/github/dependencies.go: Use functional options for REST client creation,
handle raw.NewClient error return
- pkg/github/actions.go: Handle new WorkflowDispatchRunDetails return value
from CreateWorkflowDispatchEventByID/ByFileName
- pkg/github/issues.go: Replace IssueListOptions with ListOptions for
SubIssue.ListByIssue
- pkg/github/notifications.go: MarkThreadDone now takes string instead of
int64; remove ParseInt and strconv import
- pkg/github/projects.go: Remove pointer indirection from
ListProjectsPaginationOptions and ListProjectsOptions fields
- pkg/github/issues_granular.go: Pass ctx to NewRequest, remove ctx from Do
- Test files: Add mustNewGHClient helper, replace all NewClient calls,
fix stubClientFnFromHTTP signature, fix lockdown_test.go BaseURL handling,
fix raw_test.go, remove invalid threadID test case
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Replace ingress IFC reader list with private marker
Switches the ingress IFC labels from emitting a per-repo collaborator
list to a single 'private' marker. The CLI engine now fetches readers
from the GitHub endpoint on demand at egress decision time (P-F check),
with pagination + caching, which removes a wire-bloat ceiling for repos
with thousands of collaborators.
Drops the per-call FetchRepoCollaborators from list_issues, issue_read,
get_file_contents, search_issues, and search_repositories. The shared
LabelSearchIssues helper collapses to a single []bool argument; the
intersection logic and length-mismatch failure mode go away.
This is a breaking wire-format change for _meta.ifc consumers — coordinate
with the CLI cut-over.
Refs github/copilot-mcp-core#1389.
* format
* Update FetchRepoCollaborators doc comment for marker-only ingress
Addresses Copilot review on #2478. The helper is no longer called by the
server itself; ingress emits a 'private' marker and the client engine
resolves readers on demand. Kept exported per the library-consumer
convention; updated the comment to reflect the new role.
* Address review: drop FetchRepoCollaborators and make confidentiality a scalar
Per Joanna's review on #2478:
- Remove FetchRepoCollaborators entirely (no callers left after the marker
switch). Drops the GetReposCollaboratorsByOwnerByRepo mock route too.
- Change SecurityLabel.Confidentiality from []Confidentiality to a scalar
Confidentiality. Wire format is now {integrity, confidentiality} where
confidentiality is a single 'public' or 'private' string. Updated all
tests and the LabelSearchIssues helper accordingly.
* feat: return minimal code search results with text match snippets
Return a MinimalCodeSearchResult type from search_code instead of the
raw GitHub API CodeSearchResult. This reduces token usage by ~4x by:
- Projecting the repository object to just the full_name string instead
of the full ~3KB repository payload repeated per result
- Enabling the text-match Accept header so code snippets (fragments)
are included in results, which were previously missing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: drop html_url from MinimalCodeResult
The URL is derivable from repository + path + sha, so it's redundant
token cost per result.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: add minimal_output opt-out and Accept header test for code search
Address PR review feedback:
1. Add minimal_output parameter (default: true) to search_code, matching
the pattern from search_repositories. When false, returns the full
GitHub API CodeSearchResult for backward compatibility.
2. Add Accept header assertion to tests via a new withHeaders() helper
on partialMock, verifying the text-match Accept header is actually
requested (not just mocked in the response).
3. Add test case for minimal_output=false path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: remove minimal_output opt-out from search_code
The full CodeResult only adds a bloated Repository object (~3KB of
template URLs) and a derivable HTMLURL. Nothing in the full output is
useful beyond what the minimal type already provides, so always return
the compact form.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emits an IFC SecurityLabel on the search_repositories tool result when
the InsidersMode flag is enabled, mirroring the pattern landed for
get_me (#2432), list_issues (#2453), get_file_contents (#2454),
search_issues (#2456), and issue_read (#2457).
Search results may span multiple repositories, so the join math
(integrity always untrusted; private wins by intersecting collaborator
sets across the matched private repos only) is shared with search_issues
via ifc.LabelSearchIssues. Visibility is read directly off the search
response's repo.Private field — no extra API call. Collaborators are
fetched only for private hits, and any failure causes the label to be
omitted entirely (consistent with search_issues / issue_read /
get_file_contents).
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
* Add ifc label for search_issues tool
Emits an IFC SecurityLabel on the search_issues tool result when the
InsidersMode flag is enabled, mirroring the pattern landed for get_me
in #2432, list_issues in #2453, and get_file_contents in #2454.
Search results may span multiple repositories, so the label is the IFC
join of the per-repository labels:
- Integrity is always untrusted (issues are user-authored).
- If any matched repository is public, the joined readers are
["public"] (the public side dominates the lub).
- Otherwise the joined readers are the intersection of the
collaborator sets across all matched private repositories.
- Empty result sets are labelled public-untrusted (no data leaked).
The shared searchHandler in search_utils.go gains an additive variadic
'searchOption' hook so SearchIssues can attach _meta.ifc without
duplicating the search call. SearchPullRequests is unaffected; it does
not pass any options.
If any per-repository visibility or collaborators lookup fails the label
is omitted entirely, consistent with get_file_contents, to avoid
misclassifying the result.
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
Note: this PR is chained on #2454 (gokhanarkan/fides-get-file-contents)
because it depends on the FetchRepoIsPrivate and FetchRepoCollaborators
helpers introduced there. GitHub will retarget the base to main once
#2454 merges.
* search_issues: address Copilot review findings
- LabelSearchIssues now returns (SecurityLabel, bool); the bool is
false when len(repoVisibilities) != len(readerSets), so callers can
omit the label rather than emit one computed from inconsistent
inputs.
- searchIssuesIFCPostProcess no longer substitutes [owner] when the
collaborators API returns an empty list. The substitution was
inconsistent with the cross-repo intersection semantics: the owner
could appear in another matched private repo's collaborator list and
thereby widen the joined reader set incorrectly. Empty collaborator
sets are now passed through unchanged.
- Add a subtest exercising the collaborators-failure branch (500 on
/repos/{owner}/{repo}/collaborators), asserting the tool still
succeeds and result.Meta["ifc"] is absent.
- Extend the LabelSearchIssues table tests with the slice-length
mismatch case.
Addresses the three Copilot findings on #2456.
* search_issues: flip IFC join to intersection (private wins)
Address Joanna's review feedback on #2456: a reader of a multi-repo result
must be authorised to read every matched private repository, so the IFC
join is the meet (intersection over private repos) rather than the join.
Public matches contribute the universe set and drop out of the
intersection without shrinking it.
- LabelSearchIssues: collect only the private reader sets, then intersect.
Empty result and all-public remain public-untrusted.
- TestLabelSearchIssues: flip the mixed public+private expectation and add
a 'two private + one public' case to lock in the new semantics.
- Test_SearchIssues_IFC_InsidersMode: mixed subtest now expects the
private repo's reader set instead of public.
* Add ifc label for issue_read tool
Emits an IFC SecurityLabel on the issue_read tool result when the
InsidersMode flag is enabled, mirroring the pattern landed for get_me
in #2432, list_issues in #2453, get_file_contents in #2454, and
search_issues in #2456.
issue_read operates on a single issue in a single repository so the
label has the same per-repo semantics as list_issues; the helper
ifc.LabelListIssues is reused directly. Integrity is always untrusted
(issue contents, comments, and label descriptions are user-authored).
Public repos are labelled PublicUntrusted; private repos are labelled
PrivateUntrusted with the repository's collaborator logins, falling
back to [owner] when the collaborators lookup fails.
The IssueRead handler dispatches to four sub-functions (GetIssue,
GetIssueComments, GetSubIssues, GetIssueLabels). The IFC label is
attached at the dispatch site via a single attachIFC closure, so all
four method branches emit the label without changes to the underlying
helpers. Visibility-lookup failures cause the label to be omitted
entirely (consistent with get_file_contents and search_issues).
A future cleanup PR can extract attachIFC into a shared helper now that
get_file_contents, search_issues, and issue_read use near-identical
closures; intentionally not bundled here to keep the diff minimal.
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
Note: chained on #2456 (gokhanarkan/fides-search-issues), which is in
turn chained on #2454. GitHub will retarget the base to main once those
merge.
* issue_read: simplify attachIFC by dropping unused lazy-cache
Address Joanna's review feedback on #2457: the dispatch switch returns
on exactly one branch, so attachIFC runs at most once per request. The
ifcLabelKnown / ifcIsPrivate / ifcReaders cache variables were never
reused across calls and only added complexity.
Inline the visibility and collaborators lookups directly into the
closure and drop the cache. Behaviour is identical; a follow-up can
add real per-request caching across handlers if needed.
* Add ifc label for search_issues tool
Emits an IFC SecurityLabel on the search_issues tool result when the
InsidersMode flag is enabled, mirroring the pattern landed for get_me
in #2432, list_issues in #2453, and get_file_contents in #2454.
Search results may span multiple repositories, so the label is the IFC
join of the per-repository labels:
- Integrity is always untrusted (issues are user-authored).
- If any matched repository is public, the joined readers are
["public"] (the public side dominates the lub).
- Otherwise the joined readers are the intersection of the
collaborator sets across all matched private repositories.
- Empty result sets are labelled public-untrusted (no data leaked).
The shared searchHandler in search_utils.go gains an additive variadic
'searchOption' hook so SearchIssues can attach _meta.ifc without
duplicating the search call. SearchPullRequests is unaffected; it does
not pass any options.
If any per-repository visibility or collaborators lookup fails the label
is omitted entirely, consistent with get_file_contents, to avoid
misclassifying the result.
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
Note: this PR is chained on #2454 (gokhanarkan/fides-get-file-contents)
because it depends on the FetchRepoIsPrivate and FetchRepoCollaborators
helpers introduced there. GitHub will retarget the base to main once
#2454 merges.
* search_issues: address Copilot review findings
- LabelSearchIssues now returns (SecurityLabel, bool); the bool is
false when len(repoVisibilities) != len(readerSets), so callers can
omit the label rather than emit one computed from inconsistent
inputs.
- searchIssuesIFCPostProcess no longer substitutes [owner] when the
collaborators API returns an empty list. The substitution was
inconsistent with the cross-repo intersection semantics: the owner
could appear in another matched private repo's collaborator list and
thereby widen the joined reader set incorrectly. Empty collaborator
sets are now passed through unchanged.
- Add a subtest exercising the collaborators-failure branch (500 on
/repos/{owner}/{repo}/collaborators), asserting the tool still
succeeds and result.Meta["ifc"] is absent.
- Extend the LabelSearchIssues table tests with the slice-length
mismatch case.
Addresses the three Copilot findings on #2456.
* search_issues: flip IFC join to intersection (private wins)
Address Joanna's review feedback on #2456: a reader of a multi-repo result
must be authorised to read every matched private repository, so the IFC
join is the meet (intersection over private repos) rather than the join.
Public matches contribute the universe set and drop out of the
intersection without shrinking it.
- LabelSearchIssues: collect only the private reader sets, then intersect.
Empty result and all-public remain public-untrusted.
- TestLabelSearchIssues: flip the mixed public+private expectation and add
a 'two private + one public' case to lock in the new semantics.
- Test_SearchIssues_IFC_InsidersMode: mixed subtest now expects the
private repo's reader set instead of public.
* Add optional rationale parameter to update_issue_type tool
Add an optional `rationale` string parameter (max 280 chars) to the
`update_issue_type` MCP tool. When provided, the type is sent as an
object `{"name": "...", "rationale": "..."}` to the REST API,
enabling agents to explain their classification decisions. When omitted,
existing behavior is preserved (type sent as a plain string).
This supports the agent rationale experiment for type mutations. The
parameter is always visible in the schema — the API gracefully ignores
the rationale when the server-side feature flag is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Validate issue type rationale input
* Format issue type rationale tests
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Adam Holt <omgitsads@github.com>
* Add ifc label for get_file_contents tool
Emits an IFC SecurityLabel on the get_file_contents tool result when the
InsidersMode flag is enabled, mirroring the pattern landed for get_me in
Public repositories are labelled PublicUntrusted (anyone can author file
content via pull requests). Private repositories are labelled
PrivateTrusted with the repository owner as a placeholder reader, since
only collaborators can land changes there. Full collaborator enumeration
is intentionally deferred to a follow-up shared helper.
A new exported FetchRepoIsPrivate helper wraps Repositories.Get for
visibility lookups; it is invoked lazily and only when InsidersMode is
on, so non-insiders pay no extra round trip. Visibility lookup failures
skip the label rather than fail the user-facing call.
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
* get_file_contents: address Copilot review findings
- FetchRepoIsPrivate: tighten doc to 'returns whether a repository is
private' and close the underlying *github.Response body.
- attachIFC: skip emitting the ifc label when the repository visibility
lookup fails, instead of falling through to PublicUntrusted (which
would mislabel a private or unknown-visibility repo as public). The
failure is no longer cached so a subsequent return path can retry.
- Add a test asserting the tool still succeeds and omits result.Meta ["ifc"] when the visibility lookup returns 500.
* Add ifc label for list_issues tool
Emits an IFC SecurityLabel on the list_issues tool result when the
InsidersMode flag is enabled, mirroring the pattern landed for get_me
in #2432.
Public repositories are labelled PublicUntrusted; private repositories
are labelled PrivateUntrusted with the repository owner as a placeholder
reader (full collaborator enumeration is intentionally deferred to a
follow-up shared helper).
A new IsPrivate field is added to the ListIssues GraphQL query types so
visibility is available without a second round trip.
Refs github/copilot-mcp-core#1623, github/copilot-mcp-core#1389.
* list_issues: populate readers with repo collaborators
Addresses Joanna's review feedback: for private repositories, populate
the IFC confidentiality reader set with the repository's collaborator
logins instead of the [owner] placeholder.
Adds an exported FetchRepoCollaborators helper in pkg/github/repositories.go
that paginates through Repositories.ListCollaborators. Mirrors the helper
in github-mcp-server-remote (without the cache for now; cache can land in
a follow-up).
The lookup is invoked only for private repos under InsidersMode; if it
fails we fall back to [owner] so the reader set is never empty for a
private repo.
* Add ifc labels
* Add test
* Address PR review: deterministic output, type safety, universe validation, and tests
- Fix grammar in ReadersSecurityLabelFromDict godoc
- Sort GetReaders and FiniteReaderSet.String output for determinism
- Fix godoc example to use UniversalReaders for public label
- Panic on unsupported ReaderSet types in Union/Intersection/IsSubset
- Add universe mismatch validation in PowersetLattice Join/Meet/Leq
- Add comprehensive unit tests for pkg/ifc (lattice laws, serialization, panics)
* Add a test
* Pass parameters
* Remove lattice
* Script update
* fix(mcp-apps): defer _meta.ui strip to per-request RegisterTools
The MCP Apps `_meta.ui` strip lived in `Builder.Build()`, which calls
`checkFeatureFlag(context.Background())`. The HTTP feature checker
(`createHTTPFeatureChecker`) reads insiders mode from the request
context — a background context never has it set, so the FF reported
MCP Apps off and the strip ran eagerly at server startup. Per-request
inventory factories then served pre-stripped tools regardless of
whether the request actually arrived on the `/insiders` route.
Symptom: `github/github-mcp-server-remote` returns 0 tools with
`_meta.ui` over HTTP `/insiders`, despite the source unconditionally
setting it on `get_me`, `issue_write`, and `create_pull_request`.
VS Code only renders MCP App UIs because of its persistent tool cache
from earlier deploys. Reproducible locally with
`cmd/github-mcp-server http --insiders` plus a vanilla curl tools/list.
Fix: drop the strip from `Build()`. Apply it in `RegisterTools(ctx,…)`
where the per-request context is in scope and the HTTP feature checker
can correctly detect insiders mode (or the remote checker can correctly
read user identity for Statsig flag lookup).
The same root cause affects `github/github-mcp-server-remote` — its
`featureflags.NewComposedFeatureFlagChecker` reads
`requestctx.User(ctx)`, which background context lacks, so the
`remote_mcp_ui_apps` Statsig flag always returned false. The fix here
covers both downstreams since `RegisterTools` is the single entry
point for tool registration.
Stdio mode is unaffected: it uses a closure-captured insiders mode
flag (`createFeatureChecker`) that does not depend on context, and the
per-request strip in `RegisterTools` produces the same outcome.
Verified end-to-end against the deployed remote tool definitions:
HTTP /insiders → 3 tools with _meta.ui (was 0)
HTTP / → 0 tools with _meta.ui (correct)
stdio --insiders → 3 tools with _meta.ui (unchanged)
stdio → 0 tools with _meta.ui (correct)
Adds:
- pkg/http: TestInsidersRoutePreservesUIMeta — pins the regression
- pkg/inventory: updates the existing strip tests to use the new
RegisterTools-as-strip-site contract via a captureRegisteredTools
helper
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* style: gofmt handler_test.go and registry_test.go
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* lint: address revive (context-as-argument) and unused checkFeatureFlag
- Reorder captureRegisteredTools params to put context.Context first
- Remove dead Builder.checkFeatureFlag (was only called by Build's
former MCP Apps strip, now done in RegisterTools via the Inventory
receiver's checkFeatureFlag instead)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
MCP Server Diff / mcp-diff (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
* use REST API for permission checks
* update tests
* skip API call for bots and add github-action[bot] to trusted logins
* improve tests
* add nil guard to IsSafeContent
* add comment clarifying maintain mapping
---------
Co-authored-by: Sam Morrow <info@sam-morrow.com>
GoReleaser Release / release (push) Has been cancelled
MCP Server Diff / mcp-diff (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
* Fix set_issue_fields mutation: use correct inline fragments for IssueFieldValue union
The mutation response struct used a single inline fragment
'... on IssueFieldDateValue' with a 'Name' field that doesn't exist
on that type (only IssueFieldSingleSelectValue has 'name'). This
caused GraphQL validation to fail with:
Field 'name' doesn't exist on type 'IssueFieldDateValue'
Since GraphQL validates the entire document (including response
selection sets) before executing any operation, the mutation never
fired at all — no fields were ever set regardless of input.
Fix by adding correct inline fragments for all four union types:
- IssueFieldTextValue (value)
- IssueFieldSingleSelectValue (name)
- IssueFieldDateValue (value)
- IssueFieldNumberValue (value)
* Update test mock to match corrected inline fragments
* Update handler_test.go formatting
Add NormalizeContentType middleware that strips optional parameters
(e.g. charset=utf-8) from application/json Content-Type headers before
the request reaches the Go SDK's StreamableHTTP handler, which performs
strict string matching.
Per RFC 8259, the charset parameter is redundant for JSON but must be
accepted per HTTP semantics.
Fixes#2333
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>