Skip to content

Add star list (UserList) management tools - #3200

Closed
ppoffice wants to merge 12 commits into
github:mainfrom
ppoffice:star-lists-userlist
Closed

Add star list (UserList) management tools#3200
ppoffice wants to merge 12 commits into
github:mainfrom
ppoffice:star-lists-userlist

Conversation

@ppoffice

@ppoffice ppoffice commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Adds support for managing GitHub star lists (the UserList feature at github.com/stars) to the MCP server, folding 6 new tools into the existing stargazers toolset.

Tool Inputs GraphQL Scope
list_user_lists include_items? viewer { lists } (+ node(id){...on UserList{ items }}) read:user
create_user_list name, description?, is_private? createUserList user
update_user_list name, new_name?, description?, is_private? updateUserList user
delete_user_list name deleteUserList user
add_repository_to_list owner, repo, list_name read-modify-write updateUserListsForItem user
remove_repository_from_list owner, repo, list_name read-modify-write updateUserListsForItem user

Design notes

  • List membership is independent of star state — the add/remove tools do not star/unstar anything.
  • updateUserListsForItem replaces a repository's full list membership (does not append). The add/remove tools therefore do a read-modify-write: they walk the viewer's lists to derive the repository's current memberships, merge/subtract the target list, and resubmit the complete set.
  • GitHub's schema has no reverse lookup from a repository to its lists (there is no lists field on Repository), which is why membership is derived from the list side via viewer { lists { items } }.
  • The user OAuth scope is opt-in (not in the default scope set).

Changes

  • pkg/github/user_lists.go — GraphQL helpers + 6 tool constructors.
  • pkg/github/tools.go — register the 6 tools in AllTools.
  • pkg/scopes/scopes.go — add user scope (opt-in).
  • pkg/github/user_lists_test.go — snapshot + behavioral unit tests (name→ID resolution, replace-vs-append merge logic, scope gating).
  • e2e/e2e_test.goTestUserLists exercising the full create → rename → add → verify → remove → delete lifecycle.
  • pkg/http/oauth/oauth_test.go — update TestSupportedScopes.
  • Regenerated toolsnaps and README.

Verification

  • go build ./... passes.
  • go vet -tags e2e ./e2e/ passes.
  • Star-lists unit tests pass.
  • TestUserLists verified against the live GitHub API (in-process via GITHUB_MCP_SERVER_E2E_DEBUG=true).

Adds 6 tools to the stargazers toolset for managing GitHub star
lists (the UserList feature at github.com/stars): list_user_lists,
create_user_list, update_user_list, delete_user_list,
add_repository_to_list, and remove_repository_from_list.

List membership is independent of star state, so the add/remove tools
perform a read-modify-write against updateUserListsForItem (which
REPLACES membership) without any star/unstar side-effects.

Adds the 'user' OAuth scope as opt-in (not in the default set).
GitHub's GraphQL schema has no reverse lookup from a repository to its lists (Repository has no 'lists' field), so querying repository(owner,name){ lists } fails. Derive membership by walking the viewer's lists and checking each list's items for the repository's node ID, then merge/subtract and resubmit the full set.
Adds TestUserLists, which exercises the full lifecycle against the live
GitHub API: create_user_list, update_user_list (rename),
add_repository_to_list, list_user_lists (with items, to verify
membership), remove_repository_from_list, and delete_user_list.

The test creates its own uniquely named private list and repository,
and registers cleanups for both so a failure on any step does not
leave residual state behind.
@ppoffice
ppoffice requested a review from a team as a code owner September 2, 2026 02:44
Copilot AI balanced review requested due to automatic review settings September 2, 2026 02:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unpaginated membership discovery can silently remove existing list memberships, alongside several smaller API and cleanup issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds GitHub star-list management to the existing stargazers toolset.

Changes:

  • Adds six GraphQL-backed list management tools.
  • Introduces opt-in user OAuth scope support.
  • Adds unit/E2E coverage, snapshots, and generated documentation.
File summaries
File Description
README.md Documents the new tools.
pkg/scopes/scopes.go Registers the user scope.
pkg/http/oauth/oauth_test.go Updates supported-scope expectations.
pkg/github/user_lists.go Implements list operations and tools.
pkg/github/user_lists_test.go Tests schemas and behavior.
pkg/github/tools.go Registers the tools globally.
pkg/github/__toolsnaps__/update_user_list.snap Snapshots update schema.
pkg/github/__toolsnaps__/remove_repository_from_list.snap Snapshots removal schema.
pkg/github/__toolsnaps__/list_user_lists.snap Snapshots listing schema.
pkg/github/__toolsnaps__/delete_user_list.snap Snapshots deletion schema.
pkg/github/__toolsnaps__/create_user_list.snap Snapshots creation schema.
pkg/github/__toolsnaps__/add_repository_to_list.snap Snapshots addition schema.
e2e/e2e_test.go Tests the complete list lifecycle.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 6
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/github/user_lists.go Outdated
Comment thread pkg/github/user_lists.go Outdated
Comment thread e2e/e2e_test.go Outdated
Comment thread pkg/github/user_lists.go Outdated
Comment thread pkg/github/user_lists.go
Comment thread pkg/github/user_lists.go
Fully paginate UserList item reads, including membership discovery before updateUserListsForItem replaces the full list set. Preserve explicit empty descriptions on updates, correct add/remove idempotency and destructiveness annotations, and make e2e cleanup track the active list name after rename. Update focused tests and tool snapshots accordingly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Replacement-style membership updates can lose concurrent changes, and critical pagination behavior lacks coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/scopes/scopes.go:90

  • Please assert that User is absent from DefaultOAuthScopes, as is done for the other opt-in scopes in pkg/scopes/scopes_test.go:14-19. The current supported-scope test would not catch an accidental future change that requests this write-capable scope by default.
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go
Comment thread pkg/github/user_lists.go
Comment thread pkg/github/user_lists.go Outdated
Serialize replacement-style membership updates per repository within the server process, skip already-satisfied add/remove operations, and add regression coverage for lock serialization, no-op writes, and memberships discovered beyond the first items page.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Private list results need IFC confidentiality labeling, and large-list membership scans unnecessarily repeat requests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go Outdated
Comment thread pkg/github/user_lists.go Outdated
Label user-list results as private trusted when IFC labels are enabled and continue membership pagination from the cursor already fetched. Remove the process-local membership lock and its serialization test, while retaining no-op and later-page regression coverage.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

List pagination is incomplete and OAuth scope declarations do not cover private repository access.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/github/user_lists.go:670

  • The handler resolves the repository through GraphQL, so adding a private repository requires repo access in addition to the user scope needed for the list mutation. With only the advertised user scope, the tool is exposed but fails for private repositories. Require both scopes, consistent with other repository mutation tools.

This issue also appears on line 730 of the same file.

pkg/github/user_lists.go:69

  • list_user_lists silently truncates the result to the first 100 lists while returning the full totalCount, and the tool exposes no cursor with which callers can fetch the remainder. Paginate this connection internally (as the membership path already does) or expose pagination parameters and cursor metadata.
			} `graphql:"lists(first: 100)"`

pkg/github/user_lists.go:730

  • Removing a private repository also performs a repository lookup that requires repo, but this declaration advertises and checks only user. Tokens satisfying the declared scope can therefore invoke the tool and fail on private repositories. Require both user and repo here.
		scopes.RequireAll(scopes.User),
  • Files reviewed: 15/15 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go Outdated
Comment thread pkg/github/user_lists.go Outdated
Fully paginate the viewer's UserList connection for both listing and name resolution. Make list_user_lists use a dynamic OAuth challenge so metadata requires read:user while include_items also requires repo, and add pagination and scope regression coverage.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Membership writes can omit private memberships without repo scope and can overwrite concurrent changes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

pkg/github/user_lists.go:761

  • This remove path has the same missing repo requirement as the add path: it scans all repository memberships and then replaces the complete set. A token with only user can omit private items (and may not resolve the target private repository), causing a false no-op or loss of unseen memberships. Require both user and repo.
		scopes.RequireAll(scopes.User),

pkg/github/user_lists.go:417

  • The read-modify-write is not atomic: another client can change this repository's list memberships after the scan, and this replacement mutation will silently overwrite that unrelated change. Since the tool promises a single-list add/remove, use an atomic API if one is available, or introduce a conflict-detection strategy that fails rather than submitting a stale complete set.
	input := githubv4.UpdateUserListsForItemInput{
		ItemID:  repoID,
		ListIDs: result,
	}
  • Files reviewed: 17/17 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go Outdated
Comment thread pkg/github/user_lists.go Outdated
Require repo scope for repository list membership mutations so private memberships are preserved. Fetch each list's first item page inline when include_items is requested, then continue only lists with additional pages. Update focused tests and generated scope documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Empty included lists currently omit the items field, making fetched-empty and not-fetched results indistinguishable.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go Outdated
Always serialize the items field so callers can distinguish metadata-only results (null) from an included list that was fetched and found empty ([]). Add focused JSON serialization coverage.
@ppoffice
ppoffice requested a balanced review from Copilot September 2, 2026 06:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Item-inclusive listing has unbounded API and response growth because it fully drains every list without pagination controls.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread pkg/github/user_lists.go
Expose standard cursor pagination on list_user_lists and return one bounded page of lists per call. When items are included, return at most the first 100 repositories per list together with itemsPageInfo instead of eagerly draining every nested connection. Update focused tests, tool snapshot, and generated documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Missing-repository handling, PAT scope visibility, unpageable items, and lost-update races affect correctness.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/github/user_lists.go:313

  • getRepositoryID returns the zero ID without an error when GraphQL resolves a missing repository to null. In that case add proceeds to an invalid mutation, while remove sees present == false and incorrectly reports success. Reject an empty repository ID before scanning memberships.

pkg/github/user_lists.go:410

  • The replacement mutation is based on a membership snapshot read several requests earlier. If another MCP call or GitHub client changes this repository's list memberships before this mutation, the stale ListIDs value silently erases that concurrent change. The add/remove contract needs a concurrency strategy (and, if GitHub offers no conditional mutation, the unavoidable external race should be reflected in the API rather than presented as a safe atomic add/remove).
	input := githubv4.UpdateUserListsForItemInput{
		ItemID:  repoID,
		ListIDs: result,
	}
  • Files reviewed: 17/17 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/github/tool_scopes.go Outdated
Comment thread pkg/github/user_lists.go
Require read:user visibility for classic PATs, add a dedicated cursor-paginated list_user_list_items tool for continuing item pages, reject missing repository IDs before membership scans, and assert the user scope remains opt-in. Update tests, snapshots, and generated documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Membership replacement can lose concurrent updates, and ambiguous rename failures can leak test lists.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

e2e/e2e_test.go:2028

  • Cleanup can leak the globally scoped list when the rename commits on GitHub but CallTool fails before returning: currentListName remains the old name, so cleanup never tries the renamed name. Attempt cleanup with both the tracked name and renamedList when rename completion is uncertain.

pkg/github/user_lists.go:445

  • This read-modify-write is not atomic. Two concurrent add/remove calls for the same repository can both read the same memberships and then replace them with different stale snapshots, so whichever mutation runs last silently removes the other call's update. Please serialize membership updates per repository (including across server instances, if applicable), or otherwise change the contract/implementation so concurrent updates cannot be reported as successfully preserved.
	input := githubv4.UpdateUserListsForItemInput{
		ItemID:  repoID,
		ListIDs: result,
  • Files reviewed: 19/19 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@ppoffice ppoffice closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants