* 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
Upgrades github.com/modelcontextprotocol/go-sdk from v1.5.0 to
v1.5.1-0.20260403154220-27f29c1cef3b which includes proper media type
parsing for Content-Type headers, fixing the strict string matching
that rejected application/json; charset=utf-8.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Rebase PR #2282 onto main (post-#2332) and unify feature flag
allowlists into a single source of truth.
- Add MCPAppsFeatureFlag, AllowedFeatureFlags, InsidersFeatureFlags,
and ResolveFeatureFlags in feature_flags.go
- AllowedFeatureFlags includes all user-controllable flags (MCP Apps +
granular), InsidersFeatureFlags only includes MCPAppsFeatureFlag
- HeaderAllowedFeatureFlags() now delegates to AllowedFeatureFlags
- Builder uses feature checker instead of insidersMode bool
- Remove InsidersOnly field from ServerTool and WithInsidersMode from
Builder
- HTTP feature checker uses ResolveFeatureFlags for per-request
resolution with insiders expansion
- Tool handlers check MCPAppsFeatureFlag via IsFeatureEnabled instead
of InsidersMode
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The README references docs/deprecated-tool-aliases.md which does not
exist. The correct file is docs/tool-renaming.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Upgrade the MCP Go SDK from v1.3.1-0.20260220105450-b17143f71798
(pseudo-version) to v1.5.0 (latest stable).
This also resolves#2333, as the SDK now correctly handles
Content-Type headers with MIME parameters (e.g. charset=utf-8)
via mime.ParseMediaType in StreamableHTTPHandler (added in v1.4.1).
Transitive dependency updates:
- go directive: 1.24.0 → 1.25.0 (required by SDK)
- golang.org/x/oauth2: v0.34.0 → v0.35.0
- golang.org/x/sys: v0.40.0 → v0.41.0
- segmentio/encoding: v0.5.3 → v0.5.4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>