Repository navigation
Conversation
…nnection panel - Settings in three layers: the operator configuration stays the floor, account settings (preset, read-only, default scope, tool groups) apply to every chat of a GitLab account, chat overrides from manage_context apply to the current chat only; settings only narrow what the operator allows - Restrictions apply to the tool list and to every call; numeric project and group ids are checked as the paths GitLab reports, destinations are checked too, and a scope path must be confirmed by GitLab before it is saved - Calls that name no project or group read the working scope: searches, project, merge request, vulnerability, runner, deploy key, audit and snippet listings are narrowed to a single target, or composed from each target's own listing under several targets, merged without repeats in GitLab's order and paged after the merge within a request bound; results that cannot be complete are marked partial - Scoped mark_all_done marks only the scope's todos, reading targets one after another within a request bound - browse_merge_requests list takes group_id - Every REST and GraphQL call, connection state and introspection resolve the instance of the running call: the OAuth token's, else the one the call started with; switch_instance from another chat does not move a call in flight, and in OAuth mode the session's tool list stays on the token's instance - Account settings are stored with PostgreSQL or file sessions, else in the user's local settings directory, with compare-and-set writes - New tools: get_settings and update_settings (native settings page), check_connection, open_settings_panel (MCP App), find_scope_targets, manage_context clear_scope; the channel gateway forwards resources and tool list changes - GITLAB_ALLOWED_PROJECT_IDS is enforced in runner listings and operations independently of the working scope - manage_context switch_preset, set_scope and reset now affect the current chat only Closes #637
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Important Review skippedToo many files! This PR contains 129 files, which is 29 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configuration
⛔ Files ignored due to path filters: 1
📒 Files selected for processing: 129
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2894a468bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…writes - Tests for composed project and merge request listings: which calls are composed, the shared request bound, per-target paging, repeats, the scope projects a listing reads itself and orderings that cannot merge - Tests for the scoped emulations' remaining paths: similarity order of a scope of projects, the mark_all_done bound on a target's further pages, validated calls, tools the scope does not reshape, failing todos - Tests for runner rows without an ID, project search within the request bound, catalog pages answered out of order, numeric ids a scope lists, calls the scope cannot narrow, accessors of a named instance and two settings writes of one account in a row - Drop two conditions that could never be false: runner IDs compared after repeats were removed, and the in-flight marker of a settings write, which writes one at a time leave to that write alone
- Remove manage_context list_profiles and switch_profile. A chat cannot change the account or instance its connection signed in with: in OAuth mode every call uses the token's user and instance, so switching only recorded a profile name and reported a switch that never happened. Profiles stay a startup configuration; a chat narrows itself with a preset or a scope, and another account or instance is reached by signing in to it - Drop the session's profile field, show's profileName and whoami's activeProfile, which only reported that name - Describe account settings as they work: they apply at once to every chat of the account except where a chat chose otherwise, in the tool descriptions, the settings page, the connection panel and the docs - Note in the merge request narrowing why the project and group listings keep the caller-only scope: they declare `scope` as well BREAKING CHANGE: manage_context no longer offers list_profiles and switch_profile, and its show and whoami results no longer carry profileName and activeProfile.
A working scope narrows a cross-project merge request listing to the project's own listing with the caller-only scope. The integration test unassigns one lifecycle merge request and checks that the project listing leaves it out with assigned_to_me and keeps it with all, then restores the assignee.
|



Summary
Per-account and per-chat settings for the server, with a native settings page for clients that render structured settings and an MCP App panel for clients that support app panels.
Replaces #662 (same change, squashed into one commit on the current
main).Settings model
manage_contextapply to the current chat only. Settings only narrow what the operator allows.target_project_id,target_group_id,namespace_id, a new subgroup'sparent_id, and the namespace a project is created or forked into, as the group it goes into). A scope of projects reaches no group, so group-targeted calls are refused under it.get_profile, the host's account identity, is never hidden by settings.update_settingsandmanage_context set_scoperefuse a typo or an unreachable GitLab instead of saving a guessed project or group; an expired sign-in (401, or 403insufficient_scope) fails with GitLab's error and the reauthorization challenge.GITLAB_DENIED_ACTIONS) or the preset denied stays denied under a working scope, also where the scope runs other actions in its place, and a call the scope rewrites, filters or answers by other requests is parsed by the tool's schema first, so it is validated and coerced exactly like an unscoped call. Unknown arguments are stripped by the selected action schema before target checks; another action’s arguments cannot incorrectly reject a valid call.manage_todos mark_all_donein a scoped chat marks only the scope's pending todos done, a few at a time, and reports how many were marked and which failed; the others stay pending. Each project and group of the scope has its todos read with GitLab's own project or group filter, so todos outside the scope never fill the pages read. One call marks at most 100; when more remain it is markedpartial, and a repeated call continues (a group without its subgroups also reads theirs, at most 1000 past each group's first page). Targets are read one after another within 50 requests, so a wide scope ends within the handler timeout; targets not reached are named in apartialresult.namespaceandnamespace_pathhas each checked. A snippet created without a project goes into the scope's single project; under any other scope it would be a personal snippet, and the call is refused.browse_usersstays the instance's user directory, to find people not yet in the scope (a global search of users finds its members). A project or group access token is the hard boundary GitLab enforces.include_subgroups: false, and the active-projects default of a plain listing unlessactiveorarchivedis given; or, for a plain listing under a scope made of projects, those projects themselves, archived ones left out), a global project search under a project scope answers with the active scope projects whose name, path or description match, ordered as GitLab orders it (newest created first unlesssort/order_byname another order) before pagination (at most 1000 projects; missing ordering data or an incomplete scan is reported as partial; project search has no projects scope), a global search of groups answers with the scope's groups that match, read from the namespace listing and ordered by creation as the search orders them (updated_atordering is markedpartial; project search has no groups scope and group search none before GitLab 19.4; refused under a scope of projects), a snippet title search (instance-wide only) answers with the scope's project snippets that GitLab's search matches (title, description or file name, asSnippet.searchmatches them), newest updated first, a listing of public or personal snippets (which belong to no project) and of personal access tokens (which belong to an account) are refused, and a cross-project merge request or vulnerability listing names the scope's target (a merge request listing keeps the caller-only default of the cross-project one unlessscopenames another set). A preset scope of one project or group, named alone, as a list of one or as a namespace (which covers its subgroups), is that single target. Project listings and searches (including projects only shared with the scope group), todos (also of a group the call names, whose filter covers its subgroups), cross-project merge requests and namespace listings are filtered to the scope (a namespace listing is refused under a scope of projects, which reaches no group), reading further GitLab pages until the requested page is full (at most ten pages of 100; a page still short then comes back markedpartialwith a hint to narrow the listing, and a page starting past those rows answers so without a request). Under several targets, a project listing and a cross-project merge request listing instead read each target's own listing, merged without repeats in GitLab's order and paged after the merge within ten requests (a project of the scope answers a project listing itself when the call uses onlysearch,simple,order_byandsort;similarityandpriorityorders keep target order, markedpartial). Instance-wide listings read the scope instead: deploy keys (withpublicomitted or false, GitLab's default) the deduplicated keys of the scope's projects, named or in its groups (at most ten project-enumeration pages and ten total key requests, with incomplete results markedpartial), instance audit events the trail of the scope project, or under a group or several targets the trails of the scope's projects and groups merged newest first (GitLab keeps a group's trail to its own events, so a group is read with its projects' trails and those of the subgroups on the way to them; a scope including subgroups is markedpartial, as a subgroup holding no project is not found that way), project snippets that name no project the scope project's, or under a group or several targets those of the scope's projects merged newest first, all runners those available to the scope's project or group, the caller's own runners the owned feed (with its cursor) kept to those the scope can use (markedpartialpast 1000 such runners); scopes with no such form are refused. The projects of a scope made of projects are listed in GitLab's simple representation unlesssimpleis false, read a few at a time and ordered before the page is cut. The caller's own activity (browse_events user) shows the scope project's events under a single project scope; under groups or several targets it filters the user feed by the events' resolved project paths, with at most ten feed pages and 100 distinct project lookups, reporting a partial result when either bound prevents completing the page. A namespace argument (labels, wiki, milestones) passes when it names an allowed project or an allowed group, so the scope group itself works also without its subgroups. Under a group scope without subgroups (GitLab's group results always cover them), a search and a vulnerability listing, global or naming the group itself, read the group's results kept to the group and its direct projects, in the tools' own answer shapes (vulnerabilities now carry their project's path); a user search keeps the group's own and inherited members (one member lookup per page), and a search of a group with more than 1000 direct projects comes back markedpartial. Under a scope of several projects or groups, a vulnerability listing merges the targets' listings, deduplicated and in the requested order, with a cursor bound to the scope and filters; the all-runners listing (instance-wide for administrators only) is the union of the runners available to the targets, newest first under such a cursor, and the owned-runners listing keeps its own feed and cursor, limited to runners available to one of the targets; the vulnerability and all-runners listings read each target only as far as the requested page needs. Each of these, and the composed audit trails and project snippets, shares ten requests across the scope and marks a result past that boundpartial. Under any scope, an audit listing filtered by entity follows GitLab's audit finder:ProjectorGroupkeeps the scope's trails of that kind,entity_idreads that one in-scope trail, and user or instance events answer an empty page. A global search there searches each target and merges the results without repeats within the same ten requests (groups without their subgroups list their direct projects within eleven more, shared by all of them), in GitLab's order for issues, merge requests, projects and milestones and in target order (markedpartial) for the other kinds; in a search of projects each group searches its projects and each project of the scope answers itself when its name, path or description match.allowed_toolsoverrides itsdenied_tools_regex, as documented for profiles;list-toolscounts preset tools the same way.check_connection) instead of failing every call.Runner project restrictions
GITLAB_ALLOWED_PROJECT_IDSis enforced independently of the working scope in both runner handlers. Project listing and registration validate the resolved project ID or configured full path.Instance routing
switch_instancefrom another chat does not move a call already running), else the instance the server's own token works with now (it followsswitch_instance), elseGITLAB_API_URL. Tools that built URLs from the startupGITLAB_API_URL(core, files, pipelines, iterations, namespace detection, user search, the generic client used by scope search) now reach the caller's instance;whoamireports and links it the same way, andget_profilereads the account of that instance. GraphQL clients, connection state, tier, version and token scopes resolve from the instance of the running call the same way.Storage
oauth_account_settingstable and migration), account settings live with the sessions in the database. File session storage keeps them in its file.~/.config/gitlab-mcp/settings/of the user running the server, so saved settings survive a restart. A database package older than the server, which cannot keep settings, falls back to that directory with a warning instead of failing calls.Tools and capabilities
get_settings/update_settings: the account settings page, advertised through theopenai/settingscapability. A patch is validated in full before anything is saved; administrator read-only mode cannot be turned off.check_connection: account, GitLab version and tier, restrictions in effect, the number of tools the caller can use (counted astools/listshows them, including the OAuth token's scopes), whether GitLab answered (an outage is not reported as a refused sign-in;whoamireports how its user lookup ended asuserLookup), and what to do about problems.open_settings_panel: MCP App (ui://resource,text/html;profile=mcp-app, empty CSP; the server and the channel gateway declare theio.modelcontextprotocol/uiextension) to search projects and groups, apply a scope to this chat or save it for all chats of the account, and check the connection. Follows the host theme, keyboard accessible, shows GitLab strings as text only, reports success from the settings the server holds after saving. Its preview shows the tool groups off in the chat, including those its preset turns off (manage_context showreports them), and a superseded search finishing late cannot change it. In OAuth mode listing and reading resources needsmcp:resources(refused with aninsufficient_scopechallenge otherwise), and the panel tool declares bothmcp:toolsandmcp:resources.find_scope_targets: member projects and every group the account can see (not only those it belongs to) by name, for choosing a scope (needs anapiorread_apitoken scope).browse_merge_requests listtakesgroup_id: the merge requests of a group and its subgroups (not combined withproject_id).manage_context clear_scope: the chat works everywhere, also without the account's default scope; its preset and read-only mode stay. The panel's "Work everywhere in this chat" uses it.tools/list_changednotification and advertiseslistChanged, so a host refreshes its catalog after a settings change; it also tells the host to list again after it reconnects downstream.mcp.toolscatalog inpackage.jsonis regenerated by the release PR (prepare-release.sh), as for every tool; this PR does not edit it.Behaviour change
manage_contextswitch_preset,set_scopeandresetnow affect the current chat only (they used to change the whole server process);resetreturns the chat to the account settings.manage_contextno longer offerslist_profilesandswitch_profile: a chat cannot change the account or instance its connection signed in with (in OAuth mode every call uses the token's user and instance), so switching only recorded a name;showandwhoamino longer report it. A chat narrows itself with a preset or a scope, and another account or instance is reached by signing in to it. With a static token,switch_instancekeeps every chat's read-only mode and preset, clears its scope, and points every session at the new instance before telling it to refresh its tool list.Integration test maintenance
.env.test.closing_issuestest accepts GitLab's own issue link in both forms (/-/issues/Nbefore 19.0,/-/work_items/Nfrom 19.0).removeLabelIdstest sets a known label first instead of relying on earlier tests that skip without enough lifecycle labels.set_scopeproject test scopes to a project that exists under the test group.scopefilter a narrowed listing relies on.Testing
yarn test:all, integration against GitLab 19.0 EE), lint and a compile of the build configuration pass. Tests read and write settings in a temporary directory, never the user's own settings.switch_instanceand the configured fallback.BREAKING CHANGE: manage_context no longer offers list_profiles and switch_profile, and its show and whoami results no longer carry profileName and activeProfile.
Closes #637