Add create_merge_request_note MCP server tool
What does this MR do and why?
Adds the create_merge_request_note tool to the GitLab MCP server, letting MCP agents post a
comment or a threaded reply on a merge request as the authenticated user (via the GraphQL
createNote mutation). This addresses the write half of the MR-notes gap in the issue: agents
could read MR context but had no way to participate in review conversations.
The target merge request is identified by project_id + merge_request_iid, or by a merge
request URL. Only body is unconditionally required.
Design notes
- GraphQL lives in a
.graphqlfile. The mutation is stored atapp/graphql/queries/mcp/merge_requests/create_note.mutation.graphqland loaded with theload_graphqlhelper, following #603389 (closed).spec/graphql/all_queries_spec.rbvalidates it againstGitlabSchemain CI, so a query that drifts from the schema fails the build. The operation is namedcreateMergeRequestNote(camelCase, verb-first) to match the existing mutation naming convention. - URL parsing reuses the model's own pattern. URLs are matched with
MergeRequest.link_reference_pattern— the same route-derived regex the Banzai pipeline uses to turn pasted links into references. Thenamespacecapture is the full nested path, sogroup/subgroup/projectURLs resolve correctly. No bespoke URL parser or new constant is added to the MCP tool code. - Permissions are enforced by
MergeRequestsFinder(run as the caller) and by thecreateNotemutation'screate_noteability — the tool does not re-check. - Quick-action guard: note bodies with a line beginning with
/are rejected, so a body like/mergecannot trigger MR actions throughNotes::CreateService. Mirrorscreate_workitem_note. - Conditional requirement (
urlORproject_id+merge_request_iid) is surfaced through property descriptions plus a runtime guard, not JSON SchemaanyOf— the shared MCP argument validator only rendersrequired/maxItems/enumerrors readably, soanyOfwould produce an unhelpful message. - Service naming follows the unprefixed convention (
CreateMergeRequestNoteService, noGraphqlprefix) tracked in #603096 (closed).
References
- Part of #597494 (closed) (the issue also covers the read tool; this MR delivers the write tool only, so the issue stays open).
- Stacked on !240899 (merged) (
get_merge_request_notes). This MR depends on theload_graphqlhelper and base-class changes from that MR, so it should merge after !240899 (merged). Thediscussion_idthe read tool returns feeds this tool's reply path. - Follows #603389 (closed) (move MCP tool GraphQL queries into
.graphqlfiles). - Related follow-up: #603096 (closed) (reorganize
Mcp::Toolsby domain, dropGraphqlprefix).
Screenshots or screen recordings
No UI changes.
How to set up and validate locally
-
In a Rails console (or
bin/rails runner), fetch the tool and set credentials:tool = Mcp::Tools::Manager.new.get_tool(name: 'create_merge_request_note') tool.set_cred(current_user: User.find_by_username('root'), access_token: nil) -
Pick a project + MR IID you can comment on, then post a top-level note:
mr = MergeRequest.first tool.execute(params: { arguments: { project_id: mr.project.full_path, merge_request_iid: mr.iid, body: 'Hello from MCP' } }) -
Confirm
isErrorisfalseandstructuredContent['note']containsid,url,author, anddiscussion.id. -
Identify the MR by
urlinstead (nested groups work), and reply in a thread with adiscussion_idfromget_merge_request_notes:tool.execute(params: { arguments: { url: mr.then { |m| Gitlab::UrlBuilder.build(m) }, body: 'Identified by URL' } }) tool.execute(params: { arguments: { project_id: mr.project.full_path, merge_request_iid: mr.iid, body: 'Reply', discussion_id: '<gid from get_merge_request_notes>' } }) -
Run the specs:
bundle exec rspec spec/services/mcp/tools/merge_requests/ spec/requests/api/mcp/handlers/list_tools_spec.rb ee/spec/requests/api/mcp/handlers/list_tools_spec.rb bundle exec rspec spec/graphql/all_queries_spec.rb -e 'mcp/merge_requests/create_note'
using @modelcontextprotocol/inspector
npx -y @modelcontextprotocol/inspector mise "x --" npx -y mcp-remote "https://gdk.test:3443/api/v4/mcp" --debug -e NODE_TLS_REJECT_UNAUTHORIZED=0example output
{
"note": {
"id": "gid://gitlab/Note/1550",
"body": "This note came from the MCP tool",
"internal": false,
"createdAt": "2026-06-30T14:48:38Z",
"updatedAt": "2026-06-30T14:48:38Z",
"url": "https://gdk.test:3443/top-level-public/sub-group-internal/sub-group-private/sub-project-private/-/merge_requests/1#note_1550",
"author": {
"id": "gid://gitlab/User/1",
"name": "Administrator",
"username": "root"
},
"discussion": {
"id": "gid://gitlab/Discussion/6dceb0905778193acd0545fd39c0915a5d31a361"
}
},
"errors": []
}MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
