Skip to content

Adding agents MCP tools for GitLab#673

Open
avasconcelos114 wants to merge 4 commits into
masterfrom
mcp_tool_registration
Open

Adding agents MCP tools for GitLab#673
avasconcelos114 wants to merge 4 commits into
masterfrom
mcp_tool_registration

Conversation

@avasconcelos114
Copy link
Copy Markdown
Contributor

@avasconcelos114 avasconcelos114 commented May 15, 2026

Summary

This PR registers the GitLab plugin as an MCP server with the Mattermost Agents plugin, exposing 22 tools that let AI agents read and act on GitLab data on behalf of the calling user (issues, merge requests, projects, pipelines, todos, labels, milestones, members).

All tool calls execute against the user's own OAuth token and respect the plugin's GitlabGroup namespace restriction. The /mcp endpoint is gated by pluginmcp to inter-plugin RPC only.

image

QA Notes

Please note that in order for this to be testable, you'll also need to have an agents plugin with MCP plugin registration supported, which hasn't yet been released (building from the master branch is recommended)

Once you have the agents plugin setup you should be able to see the MCP tools appearing in the list, after that you may prompt Matty to:

  • List out projects or issues
  • Get a list of your Gitlab todos
  • Get a list of active merge requests
  • Create an issue on your behalf, assigning it a label or assignee
  • Add a comment on an issue
  • List and trigger a pipeline to run

Ticket Link

Change Impact: 🟠 Medium

Reasoning: Adds an MCP server surface and 22 MCP tools that enable inter-plugin RPC using callers' OAuth tokens, extends the GitLab client interface, and updates mocks and tests—significant new capability that is mostly localized to the plugin but touches auth and inter-plugin surfaces.
Regression Risk: Moderate — touches authentication/authorization flows (caller resolution, token refresh), registers a new /mcp HTTP endpoint gated by pluginmcp permission, expands public interfaces and generated mocks, and introduces substantial new handler logic; changes are mostly additive and include unit tests, but the inter-plugin and token-scoped nature increases risk.
** QA Recommendation:** Manual QA recommended: build and install an Agents plugin supporting MCP registration, verify MCP tools appear and are permission-gated, test representative flows (list/create/update issues & MRs, add comments, list/trigger pipelines, fetch todos/dashboard), and confirm correct caller resolution and token usage. Skipping manual QA is not advised given the authentication and inter-plugin surface.

Generated by CodeRabbitAI

Review Change Stack

@avasconcelos114 avasconcelos114 self-assigned this May 15, 2026
@avasconcelos114 avasconcelos114 requested a review from a team as a code owner May 15, 2026 15:53
@avasconcelos114 avasconcelos114 added 2: Dev Review Requires review by a core committer 3: QA Review Requires review by a QA tester labels May 15, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

Adds an MCP server and HTTP /mcp routing, extends the GitLab interface, implements GitLab MCP API methods and handlers, registers MCP tools and schemas, updates generated mocks, and adds unit tests; also updates module dependencies in go.mod.

Changes

MCP Server and GitLab Integration

Layer / File(s) Summary
Dependency updates
go.mod
Bumps Go toolchain and advances direct and transitive dependencies.
GitLab interface extension
server/gitlab/gitlab.go
Adds exported method signatures for updating issues, adding notes, searching/creating merge requests, and listing project pipelines.
MCP GitLab API implementation
server/gitlab/mcp_api.go
Implements the new GitLab methods with GitLab client connect, allowed-group enforcement, go-gitlab calls with context, response checks, and wrapped errors.
MCP lifecycle, routing & caller resolution
server/mcp.go, server/plugin.go, server/api.go
Adds lazy MCP start/stop with mutex, registers /mcp HTTP route to serveMCPHTTP, integrates start/stop in OnActivate/OnDeactivate, and resolves callers to GitLab OAuth tokens.
MCP tool schemas & registration
server/mcp_tools.go
Introduces JSON-schema-annotated MCP input/output structs for issues, MRs, projects, pipelines, todos, labels/milestones/members and registers all tools via pluginmcp.AddTool.
MCP tool handlers and helpers
server/mcp_handlers.go
Implements handlers for issues/MRs/projects/pipelines/todos/dashboard/labels/milestones/members with input validation, caller resolution, GitLab calls, conversions to summary types, note URL helper, and project-path parsing.
Mocks and tests
server/gitlab/mocks/mock_gitlab.go, server/mocks/mock_gitlab.go, server/mcp_test.go
Adds mocks/recorder helpers for new GitLab methods and switches variadic recorder param types to any; adds unit tests covering MCP lifecycle, handler validation, resolveCaller, path parsing, and conversion helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through code to stitch a new gate,
MCP wakes and listens at /mcp’s slate,
Issues, MRs, pipelines tied with care,
Tools, mocks, and tests all placed and fair,
A rabbit's small patch, tidy and great.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Adding agents MCP tools for GitLab' clearly summarizes the main change: implementing MCP tool registration for GitLab operations within the Mattermost plugin, which is the primary focus of the PR across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
server/mcp_handlers.go (2)

302-324: ⚡ Quick win

Consider populating the WebURL field in AddMergeRequestCommentOutput.

Similar to the issue comment handler, the AddMergeRequestCommentOutput struct defines a WebURL field (line 164-167 in mcp_tools.go), but line 323 doesn't populate it from the returned Note.

Proposed enhancement
-	return nil, AddMergeRequestCommentOutput{NoteID: note.ID, Body: note.Body}, nil
+	return nil, AddMergeRequestCommentOutput{
+		NoteID: note.ID,
+		Body:   note.Body,
+		WebURL: note.NoteableWebURL,
+	}, nil

Note: Verify the exact field name in gitlab0.Note (might be NoteableWebURL, WebURL, or similar).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/mcp_handlers.go` around lines 302 - 324, The
AddMergeRequestCommentOutput returned by handleAddMergeRequestComment doesn't
set WebURL; update handleAddMergeRequestComment to populate
AddMergeRequestCommentOutput.WebURL from the note returned by
GitlabClient.AddMergeRequestNote (e.g., map to the correct field on the returned
note such as NoteableWebURL, WebURL, or equivalent), ensuring the output
includes NoteID, Body and WebURL before returning.

154-176: ⚡ Quick win

Consider populating the WebURL field in AddIssueCommentOutput.

The output struct (line 90-94 in mcp_tools.go) includes a WebURL field, but the handler doesn't populate it. The gitlab0.Note returned from AddIssueNote likely contains URL information that should be included in the response for better user experience.

Proposed enhancement
-	return nil, AddIssueCommentOutput{NoteID: note.ID, Body: note.Body}, nil
+	return nil, AddIssueCommentOutput{
+		NoteID: note.ID,
+		Body:   note.Body,
+		WebURL: note.NoteableWebURL,
+	}, nil

Note: Verify the exact field name in gitlab0.Note (might be NoteableWebURL, WebURL, or similar).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/mcp_handlers.go` around lines 154 - 176, The handler
handleAddIssueComment currently returns AddIssueCommentOutput without setting
the WebURL; update it to extract the appropriate URL from the gitlab0.Note
returned by GitlabClient.AddIssueNote (e.g., note.WebURL or note.NoteableWebURL
— verify the exact field name on gitlab0.Note) and assign that value to
AddIssueCommentOutput.WebURL before returning, so the response includes the
note's web link.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/gitlab/mcp_api.go`:
- Around line 47-54: The code dereferences opts without nil checks when building
updateOpts (internGitlab.UpdateIssueOptions) and in the merge request creation
path; add a nil guard before accessing any opts fields (e.g., check if opts ==
nil and return an error or use default empty values) in the functions that build
updateOpts and the merge request creation routine so you avoid panics; locate
the builder that constructs updateOpts and the merge request creation code (the
scopes that reference opts, Title, Description, StateEvent, AssigneeIDs, Labels,
MilestoneID and the MR-equivalent fields) and either early-return an error when
opts is nil or initialize a safe zero-value struct and then reference its
fields.
- Line 39: Multiple methods in server/gitlab/mcp_api.go dereference the OAuth
token pointer (e.g., the call site client, err := g.GitlabConnect(*token>) )
without nil checks which can panic; update each method that uses the token
pointer (the occurrences at the shown call sites and similar lines) to first
check if token == nil and return a controlled authentication error (or nil
client + descriptive error) instead of dereferencing, and only call
GitlabConnect(*token) after the nil check; reference the token parameter in
these functions and the GitlabConnect call sites to locate and fix each
occurrence.

---

Nitpick comments:
In `@server/mcp_handlers.go`:
- Around line 302-324: The AddMergeRequestCommentOutput returned by
handleAddMergeRequestComment doesn't set WebURL; update
handleAddMergeRequestComment to populate AddMergeRequestCommentOutput.WebURL
from the note returned by GitlabClient.AddMergeRequestNote (e.g., map to the
correct field on the returned note such as NoteableWebURL, WebURL, or
equivalent), ensuring the output includes NoteID, Body and WebURL before
returning.
- Around line 154-176: The handler handleAddIssueComment currently returns
AddIssueCommentOutput without setting the WebURL; update it to extract the
appropriate URL from the gitlab0.Note returned by GitlabClient.AddIssueNote
(e.g., note.WebURL or note.NoteableWebURL — verify the exact field name on
gitlab0.Note) and assign that value to AddIssueCommentOutput.WebURL before
returning, so the response includes the note's web link.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b9314b6b-9d42-4a40-8944-ef703d97aff9

📥 Commits

Reviewing files that changed from the base of the PR and between 696fae5 and b4ea430.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • go.mod
  • server/api.go
  • server/gitlab/gitlab.go
  • server/gitlab/mcp_api.go
  • server/gitlab/mocks/mock_gitlab.go
  • server/mcp.go
  • server/mcp_handlers.go
  • server/mcp_test.go
  • server/mcp_tools.go
  • server/mocks/mock_gitlab.go
  • server/plugin.go

Comment thread server/gitlab/mcp_api.go
Comment thread server/gitlab/mcp_api.go
@avasconcelos114
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avasconcelos114
Copy link
Copy Markdown
Contributor Author

@coderabbitai approve

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

✅ Actions performed

Comments resolved and changes approved.

Comment thread server/mcp.go
p.registerTools(s)
p.mcpServer = s

if err := s.Register(); err != nil {
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.

Register() returns nil immediately and retries in a goroutine and this branch cant fire. Either delete it or replace the body with a comment :)

Comment thread server/mcp.go
return nil, nil, fmt.Errorf("GitLab account not connected: %s", apiErr.Message)
}

token, err := p.getOrRefreshTokenWithMutex(info)
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.

This DMs the user when the refresh fails and with an LLM in the loop the agent will retry a tool call that just failed and we'll DM the same user a few times. Worth considering skip the DM when called from the MCP path. Not blocking.

Comment thread server/mcp_tools.go
@@ -0,0 +1,432 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
Copy link
Copy Markdown
Contributor

@nang2049 nang2049 May 18, 2026

Choose a reason for hiding this comment

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

22 tools is too many. The pluginmcp README says that 10 is the budget per plugin because every tool's schema goes into the system prompt of every LLM call the agent makes. I see a few obvious merges to get us under but worth considering stripping more

1.list_my_assigned_issues and search_issues can become one list_issues with assigned_to_me and search fields.
2. list_my_assigned_merge_requests, list_my_review_requests and search_merge_requests can become list_merge_requests.
3. list_project_labels, list_project_milestones and list_project_members can be one get_project_metadata with a kind enum.
4. get_gitlab_dashboard I'd just drop

Comment thread server/mcp_tools.go

func (p *Plugin) registerTools(s *pluginmcp.Server) {
// Issues
pluginmcp.AddTool(s, &mcp.Tool{
Copy link
Copy Markdown
Contributor

@nang2049 nang2049 May 18, 2026

Choose a reason for hiding this comment

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

No annotations on any of these tools. Without ReadOnlyHint or DestructiveHint the agents can't tell that get_issue is safe to auto-run. Read tools want ReadOnlyHint: true. The add_*_comment and create_* calls need DestructiveHint: ptr(false). run_pipeline I'd actually mark destructive since it can kick off CI that costs money

Comment thread server/mcp_handlers.go
return nil, GetIssueOutput{Issue: issueToSummary(issue.Issue)}, nil
}

func (p *Plugin) handleListMyAssignedIssues(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, ListMyAssignedIssuesOutput, error) {
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.

for the list endpoints (this one, handleListMyAssignedMRs, handleListMyReviewRequests, handleListMyProjects, handleGetMyTodos) we ship no pagination but the agent has no way to ask for more than GitLab's default page. Proposing to embed PaginationInput like ListProjectPipelinesInput does or document in the description that these are capped.

Comment thread server/mcp_handlers.go
return nil, ListMyAssignedIssuesOutput{}, err
}

client, err := p.GitlabClient.GitlabConnect(*token)
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.

Every other handler hands the token to the gitlab package and lets it construct the client internally. These four reach in directly. A little concerned its the only place we leak the raw client out of the gitlab package.

Comment thread server/mcp_handlers.go
Labels: internGitlab.LabelOptions(in.Labels),
}

owner, repo := splitProjectPathParts(in.ProjectPath)
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.

nit: Use splitProjectPath here so the agent gets the actionable "must be in namespace format" message before we go to GitLab. Same in handleGetProject.

Comment thread server/mcp_handlers.go
if baseURL == "" || projectPath == "" {
return ""
}
return fmt.Sprintf("%s/%s/-/%s/%d#note_%d", baseURL, projectPath, kind, parentIID, noteID)
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.

If GitlabURL ends with a slash you get https://gitlab.com//ns/proj/ it returns the canonical URL on the note itself for newer gitlab versions. I would check whether note.URL or note.WebURL is populated and use that, or minimum strings.TrimRight(baseURL, "/") here.

Comment thread server/mcp_test.go

// --- Handler validation tests (mocked GitlabClient) ------------------------

func newPluginWithMockGitlab(t *testing.T) (*Plugin, *mockgitlab.MockGitlab) {
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.

Can we add end to end test that exercises pluginmcp.Server.ServeHTTP with a real tools payload? Validation tests are useful but we need the gitlab__ name prefix actually appearing on the wire and the schema we generate from our struct tags being valid and annotations once we add them.

Comment thread server/gitlab/gitlab.go
allowPrivate bool,
) (namespace string, project string, err error)

UpdateIssue(ctx context.Context, user *UserInfo, token *oauth2.Token, projectID string, issueIID int, opts *UpdateIssueOptions) (*internGitlab.Issue, error)
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.

Nit: I'd split out a GitlabMCP interface for the new ones.

Comment thread server/mcp_tools.go
// Issues
pluginmcp.AddTool(s, &mcp.Tool{
Name: "get_issue",
Description: "Retrieve details of a single GitLab issue by project path and issue IID (the number shown in the GitLab UI). Returns title, state, description, labels, assignees, milestone, and web URL.",
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.

The descriptions are doing two things they shouldn't and missing one thing they should. First, and I also struggle with where to draw this line too tbh :D took me a minute to look it up. They restate the input schema "by project path and issue IID", "Use state_event 'close' or 'reopen'" and the output struct fields "Returns title, state, description, labels, assignees, milestone, and web URL". The JSON Schema auto generated from your structs already carries all of that ans the LLM gets it from there.

What's missing is sibling disambiguation. With 22 tools the LLM's actual problem is "when do I pick get_issue vs search_issues vs list_my_assigned_issues". That's what the description should answer.

Sources that i read but still struggle to separate it for myself :D

https://modelcontextprotocol.io/specification/2025-06-18/server/tools - description is for the LLM's understanding.
https://github.com/ProfessionalWiki/MediaWiki-MCP-Server/blob/master/docs/tool-conventions.md is verb phrase, what it returns, sibling routing, no schema duplication.
https://github.com/googleapis/mcp-toolbox/blob/main/docs/en/reference/style-guide.md which explicitly says don't put imperatives in descriptions ("IMPORTANT: you MUST…") because they're prompt-injection-shaped.

so this line will be something like: Description: "Returns a single issue's details. For keyword lookup use search_issues; for the calling user's open issues use list_my_assigned_issues.",

Copy link
Copy Markdown
Contributor

@nang2049 nang2049 left a comment

Choose a reason for hiding this comment

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

Thanks @avasconcelos114 mostly it looks great! As I worked with a few MCP's and still struggle to define best practices for registration, I added some comments that I have seen along the way for 'proper' use. Feel free to connect if you want to aling on the approach 🙏

@avasconcelos114
Copy link
Copy Markdown
Contributor Author

@nang2049 First of all thank you for the tremendous review, I haven't worked much with MCP's so your review is pure gold, I'll get to addressing each of the points and might reply in specific threads if anything isn't clear to me, many thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2: Dev Review Requires review by a core committer 3: QA Review Requires review by a QA tester

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants