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 .graphql file. The mutation is stored at app/graphql/queries/mcp/merge_requests/create_note.mutation.graphql and loaded with the load_graphql helper, following #603389 (closed). spec/graphql/all_queries_spec.rb validates it against GitlabSchema in CI, so a query that drifts from the schema fails the build. The operation is named createMergeRequestNote (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. The namespace capture is the full nested path, so group/subgroup/project URLs 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 the createNote mutation's create_note ability — the tool does not re-check.
  • Quick-action guard: note bodies with a line beginning with / are rejected, so a body like /merge cannot trigger MR actions through Notes::CreateService. Mirrors create_workitem_note.
  • Conditional requirement (url OR project_id + merge_request_iid) is surfaced through property descriptions plus a runtime guard, not JSON Schema anyOf — the shared MCP argument validator only renders required/maxItems/enum errors readably, so anyOf would produce an unhelpful message.
  • Service naming follows the unprefixed convention (CreateMergeRequestNoteService, no Graphql prefix) 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 the load_graphql helper and base-class changes from that MR, so it should merge after !240899 (merged). The discussion_id the read tool returns feeds this tool's reply path.
  • Follows #603389 (closed) (move MCP tool GraphQL queries into .graphql files).
  • Related follow-up: #603096 (closed) (reorganize Mcp::Tools by domain, drop Graphql prefix).

Screenshots or screen recordings

No UI changes.

How to set up and validate locally

  1. 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)
  2. 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' } })
  3. Confirm isError is false and structuredContent['note'] contains id, url, author, and discussion.id.

  4. Identify the MR by url instead (nested groups work), and reply in a thread with a discussion_id from get_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>' } })
  5. 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=0

image

example 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.

Edited by Terri Chu

Merge request reports

Loading
Loading