Skip to main content
Glama
gitlab-readonly-mcp
by igordev96

gitlab-readonly-mcp

CI Release

A small, read-only, git-backed MCP server for GitLab (self-managed or gitlab.com).

It lets an AI agent read files from any branch, tag, or commit, including branches that are not checked out locally, without touching your working tree. Content comes from a local bare mirror the server keeps in a cache directory, or from a clone of your own when you register one.

The install is one command, with no checkout and no build:

npm install -g gitlab-readonly-mcp

Install notes: Windows, nvm, the form that needs no install at all, and how to uninstall.

Who this is for

Use this when

  • the only token you can get is a project access token or a legacy PAT. The official GitLab MCP server rejects both. It needs OAuth with the mcp scope, or a fine-grained PAT with the MCP tool resource. This server needs only read_repository, and has no write path that could use more;

  • your instance has no Advanced Search, either because it is Free or because a self-managed instance does not configure it. The REST API's code search needs the blobs scope. Search, blame, pickaxe and compare run over git and cost nothing, so one fetch per TTL serves every read instead of an API call per tool;

  • the official server is unavailable: your GitLab is older than 18.3, or the instance admin (on gitlab.com, a top-level group Owner) has not enabled it. This server works on self-managed and gitlab.com alike, as long as the repository can be cloned over HTTPS with a read-only token;

  • you need a branch that is not checked out, or a file from a commit that never reached your machine, and you do not want to disturb the working tree you have.

Not this when

  • you can use the official GitLab MCP server and want its full feature set. It covers more, and it is the better first choice when your token and permissions allow it;

  • the agent has to change something: write a file, comment, open a merge request. This server is read-only on purpose;

  • you need merge requests, issues, pipelines or CI logs. It reads version control, not the tracker.

Related MCP server: GitLab MCP Server

What it is not

  • One GitLab instance per server. GITLAB_HOST names a single host; a second instance means a second server.

  • No write path. No configuration enables writing: there is no tool that writes, and a read_repository token could not use one if there were.

  • Mirrors are full clones. A project costs its repository size on disk. blobless is refused, and the reason is in the risks table. A repository you already have costs nothing: register it with add_repo and it is read from there.

  • Not a git client. It never checks anything out, never writes to your working tree, and never pushes.

Evidence. Each release was verified against a live GitLab. The milestones table in docs/history.md records what was checked, what failed, and what is still unverified.

Why git instead of the GitLab REST API?

Reading a file at a ref, comparing branches, history, blame and code search are git's strengths. Without Advanced Search, the REST API is weak at most of them. How it works has the longer answer.

Requirements

  • Node.js 24+ (developed on Node 24.21)

  • git on PATH

  • A GitLab token with the read_repository scope (a personal or project access token). Never use api or write_repository.

Install

From npm:

npm install -g gitlab-readonly-mcp

If the machine cannot reach the npm registry, install the tarball attached to the latest release instead. No checkout, no build, no registry:

npm install -g https://github.com/igordev96/gitlab-readonly-mcp/releases/download/v0.11.0/gitlab-readonly-mcp-0.11.0.tgz

Each release carries SHA256SUMS beside the tarball if you want to check the download before installing it. The Source code archives GitHub attaches to a release are repository snapshots without dist/, so they install nothing.

To work on the server instead, build the tarball from your checkout:

npm install
npm pack --pack-destination .          # gitlab-readonly-mcp-0.11.0.tgz
npm install -g .\gitlab-readonly-mcp-0.11.0.tgz

Either way, gitlab-readonly-mcp lands on PATH and speaks MCP on stdio from any directory. To upgrade, npm install -g gitlab-readonly-mcp@latest, or install the newer release's tarball.

To uninstall, npm rm -g gitlab-readonly-mcp.

Under nvm, global packages live inside the active Node version, so switching versions hides the command until you reinstall it.

Without a global install. npx runs the published package from npm's cache:

npx -y gitlab-readonly-mcp

On a machine with no registry access, point npx at the release tarball instead:

npx -y --package https://github.com/igordev96/gitlab-readonly-mcp/releases/download/v0.11.0/gitlab-readonly-mcp-0.11.0.tgz gitlab-readonly-mcp

The first start downloads it; later starts take about a second. In a client config the npm form is "command": ["npx", "-y", "gitlab-readonly-mcp"]; the tarball form adds --package <tarball URL> before the name.

Configuration

All configuration is via environment variables.

Variable

Required

Description

GITLAB_HOST

yes

Base URL of the instance, e.g. https://gitlab.example.com or https://gitlab.com (no trailing slash). Project paths are not unique per instance, so the host must be explicit.

GITLAB_TOKEN

yes

Read-only token (read_repository). Works with project access tokens and legacy PATs that the official GitLab MCP server rejects. Injected per git call via http.extraheader; never written to .git/config.

GITLAB_DEFAULT_PROJECT

no

Project path used when a tool call omits project, e.g. group/subgroup/repo.

GITLAB_MCP_CACHE

no

Cache root for mirrors. Defaults to %LOCALAPPDATA%\gitlab-readonly-mcp (or ~/.cache/... elsewhere).

GITLAB_CA_INFO

no

Path to a CA bundle, if the server uses a private CA.

GITLAB_GIT_BIN

no

Git executable. Defaults to git.

GITLAB_FETCH_MODE

no

Default freshness policy for every project: ttl (default) fetches only when a mirror is older than the TTL, always fetches before every read, manual never fetches by itself. A project can override it in repos.json.

GITLAB_FETCH_TTL_SECONDS

no

Age in seconds after which a mirror counts as stale. Default 600 (10 minutes). 0 behaves like always.

These are the defaults; a project can override its freshness policy in repos.json (see Repositories and policy).

Wire into OpenCode

opencode mcp add gitlab --global `
  --env GITLAB_HOST=https://gitlab.example.com `
  --env 'GITLAB_TOKEN={env:GITLAB_TOKEN}' `
  --env GITLAB_DEFAULT_PROJECT=group/subgroup/repo `
  -- gitlab-readonly-mcp

Or add it to configuration by hand (mcp.servers):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "gitlab": {
        "type": "local",
        "command": ["gitlab-readonly-mcp"],
        "environment": {
          "GITLAB_HOST": "https://gitlab.example.com",
          "GITLAB_TOKEN": "{env:GITLAB_TOKEN}",
          "GITLAB_DEFAULT_PROJECT": "group/subgroup/repo"
        }
      }
    }
  }
}

The command is the installed binary, so the entry point does not depend on where the checkout lives. A global MCP server's working directory is the workspace it is launched from, not this project. If the bare name does not resolve on your machine, npm prefix -g gives the prefix; <prefix>\gitlab-readonly-mcp.cmd is the same package by absolute path.

["npx", "-y", "gitlab-readonly-mcp"] also works, installed or not: npx uses the binary on PATH when there is one, and otherwise downloads the package from npm. If the machine cannot reach the registry, use the tarball form from Install.

The tools appear as gitlab_<tool>, such as gitlab_get_file and gitlab_log. All sixteen are listed below.

Wire into Cursor

Cursor reads MCP servers from a mcpServers object, in .cursor/mcp.json for one project or ~/.cursor/mcp.json for every project (%USERPROFILE%\.cursor\mcp.json on Windows). It starts a command you have installed, or npx -y gitlab-readonly-mcp if you have not.

{
  "mcpServers": {
    "gitlab": {
      "command": "npx",
      "args": ["-y", "gitlab-readonly-mcp"],
      "env": {
        "GITLAB_HOST": "https://gitlab.example.com",
        "GITLAB_TOKEN": "${env:GITLAB_TOKEN}",
        "GITLAB_DEFAULT_PROJECT": "group/subgroup/repo"
      }
    }
  }
}

The form above was tested on Windows, where it starts the server through the npx shim. If Cursor reports ENOENT, wrap it in cmd /c. Clients that start the command without a shell cannot execute a .cmd file, so the server never appears and MCP Logs is the only place that says so:

"command": "cmd",
"args": ["/c", "npx", "-y", "gitlab-readonly-mcp"],

Once the package is installed, "command": "gitlab-readonly-mcp" is equivalent to the npx form on every platform. npx adds a wrapper process that resolves the same binary, from PATH when it is installed and from the npm registry when it is not. If the machine cannot reach the registry, use the --package form from Install instead, where args becomes ["-y", "--package", "<tarball URL>", "gitlab-readonly-mcp"].

Quit Cursor completely and reopen it; a window reload is not always enough. The server then shows under Settings → MCP, and anything it printed on the way up is in Output → MCP Logs, the first place to look when the entry exists but its tools do not.

Two traps are silent:

  • The interpolation syntax is not OpenCode's. Cursor resolves ${env:NAME}; OpenCode resolves {env:NAME}. Copy a config from one to the other unchanged and the literal string is passed as the token, which GitLab rejects.

  • ${env:GITLAB_TOKEN} reads Cursor's own environment, not your shell's history: a variable set only in the current terminal is invisible to an app started from the taskbar. Use the OS-level variable, or point envFile at a file: "envFile": "${workspaceFolder}/.env". A token pasted literally works too, but then .cursor/mcp.json must stay out of git.

What it can do

Tool

What it answers

Files and trees

get_file

One file at a ref, the whole file or a line range. Accepts a blob, raw or blame link.

list_tree

A directory at a ref, one level deep or recursive. Accepts a tree link.

Refs

list_refs

Branches and tags, with the commit each points at.

resolve_ref

What a branch, tag or sha resolves to, with close matches when the name is wrong.

Search

search

Grep a ref's tree, grouped by file: matched lines, file paths, or per-file counts.

pickaxe

The commits whose diff changed how often a text appears.

History and diffs

log

A ref's history, optionally for one path. Accepts a history link.

get_commit

One commit: message, parents, author, files, line counts, and the patch on request. Accepts a commit link.

blame

Who last touched each line, grouped by commit. Accepts a blame link.

compare

What changed on one ref since it split from another, as a capped file summary.

diff_file

One file's patch between two refs.

Links

parse_gitlab_url

What a GitLab link names (project, target, ref, path and line range), without reading anything.

Repository policy

list_repos

Every project in use: its source, its policy and its true age.

sync_repo

Fetch now, ignoring the TTL.

add_repo

Read a project from a clone of your own instead of a mirror.

remove_repo

Forget a project. Never deletes your clone.

Quickstart

Examples use group/sub/repo; set GITLAB_DEFAULT_PROJECT to leave project= out. Every result says which commit answered and how fresh it is, which What the odd bits mean explains.

Read a file from a branch you have not checked out. There is no clone of your own to make, and your working tree stays as it is:

gitlab_get_file(project="group/sub/repo", ref="fix/PROJ-7608", path="src/index.tsx")
<file project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" path="src/index.tsx" synced_at="2026-10-18T09:12:44.318Z">
…file contents…
</file>

Browse a tree before reading. Files carry their size, so a small config is easy to tell from a bundle. recursive=true walks the whole subtree:

gitlab_list_tree(project="group/sub/repo", ref="fix/PROJ-7608", path="src")
<tree project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" path="src" count="3" synced_at="…">
blob	3047	src/App.tsx
tree	-	src/components
blob	412	src/theme.css
</tree>

Find and confirm a branch. list_refs searches names; resolve_ref turns one into a commit, and suggests close matches when the name is wrong:

gitlab_list_refs(project="group/sub/repo", kind="branches", search="PROJ")
<refs project="group/sub/repo" kind="branches" default_branch="develop" count="1" synced_at="…">
branch	abc123def456	fix/PROJ-7608	PROJ-7608: adjust the header
</refs>

Find where something is used without a clone or a file path:

gitlab_search(project="group/sub/repo", ref="fix/PROJ-7608", query="orderTotals")
<search project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" query="orderTotals" count="2" files="1" synced_at="…">
src/reports/OrderTotals.tsx
  22: export const OrderTotalsReport = () => {
  64:   title={translations.reporting.seeOrderTotalsPage}
</search>

output="count" answers "how many, in which files" without listing every match; context=2 adds the lines around each hit. pickaxe(query=…) answers the follow-up: when did this text appear?

Read a pasted link, including the lines it names. A #L118-126 anchor becomes the range:

gitlab_get_file(url="https://gitlab.example.com/group/sub/repo/-/blob/fix/PROJ-7608/src/index.tsx#L118-126")
<file project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" path="src/index.tsx" lines="118-126" file_lines="373" synced_at="…">
118: …
</file>

Each link kind has one reader: blob, raw and blame links go to get_file (or blame), tree links to list_tree, /-/commit/ to get_commit, /-/commits/ to log. A link handed to another tool is refused by name and says which tool wants it. parse_gitlab_url(url) reports what a link names without reading anything, and resolve=false does it offline.

See the history, and who last touched a line. path is optional and is a git pathspec, so globs work:

gitlab_log(project="group/sub/repo", ref="fix/PROJ-7608", path="src/index.tsx")
<log project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" path="src/index.tsx" count="2" synced_at="…">
abc123def4	2026-10-15T12:32:33+01:00	A. Author	PROJ-7608: adjust the header
def456abc7	2026-10-14T09:15:02+01:00	B. Author	Merge branch 'develop' into fix/PROJ-7608	(merge)
</log>
gitlab_blame(project="group/sub/repo", ref="fix/PROJ-7608", path="src/index.tsx", lines="118-126")
<blame project="group/sub/repo" ref="fix/PROJ-7608" commit="abc123…" path="src/index.tsx" lines="118-126" file_lines="373" groups="2" count="9" max_lines="500" synced_at="…">
def456abc789	B. Author	2026-04-23T10:11:50+02:00	118-121
  118: …
abc123def456	A. Author	2026-10-15T12:32:33+01:00	122-126
  122: …
</blame>

get_commit(rev=…) reads one commit: message, files, line counts, and the patch with patch=true.

Compare two branches. compare prints the merge base it used, so which diff ran is never hidden:

gitlab_compare(project="group/sub/repo", from="develop", to="fix/PROJ-7608")
<compare project="group/sub/repo" from="develop" to="fix/PROJ-7608" from_commit="…" to_commit="…" merge_base="def456abc789…" files="1" total_files="1" additions="32" deletions="11" max_files="100" synced_at="…">
M	src/index.tsx	+32 -11
</compare>

diff_file(from=…, to=…, path=…) is one file's patch; pass the merge_base it printed as from for the branch's own view of the file.

Gotchas

  • search and pickaxe match literally and case-insensitively: foo|bar looks for that text. Pass regex=true for alternation, case_sensitive=true for exact case.

  • A plain pathspec's ** needs a directory level, so src/**/*.tsx misses src/index.tsx. Use :(glob)src/**/*.tsx for zero or more.

  • Omit ref to read the default branch, which is what a fresh clone shows you.

  • #L12 and #L12-20 anchors are 1-based and inclusive.

  • compare is three-dot by default; merge_base=false asks for the raw two-dot diff.

  • Binary files are refused where the answer would be meaningless (a read, a blame); a diff gives git's Binary files … differ line instead of a patch.

  • Caps are always reported: truncated="true", total=… and [cut: …] say when an answer was shortened.

What the odd bits mean

You see

It means

synced_at="…"

When the source that answered last fetched successfully. unknown means no successful fetch has ever stamped it.

refresh_failed="true"

A refresh was due and failed, so the answer came from the cache; a [refresh failed: …] note says why.

truncated="true" / total=…

The answer was capped. Where the tool can count it, total is the real number before the cap.

ref_confirmed="false"

parse_gitlab_url could not match the link's ref against the project's refs: it is offline, or the link names a sha. commit="…" is the resolved revision when the link names one.

merge_base="none"

The two refs share no history, so compare ran the raw two-dot diff and said so.

(no matches)

Nothing matched. That is an answer, not an error.

Repositories and policy

A project registers itself the first time it is used, when its mirror is cloned on demand, or explicitly with add_repo. The registry is a small repos.json in the cache directory:

<repos count="2">
group/sub/repo	source=mirror	freshness=ttl	ttl=600s	present=true	synced_at="2026-10-18T21:44:25.655Z"
group/sub/other	source=local	freshness=ttl	ttl=600s	present=true	local_path=C:\work\other	synced_at="unknown"
</repos>

synced_at is when that source last fetched successfully, or unknown when no fetch has ever stamped it; present=false means the project is registered but its data is not on disk, because a mirror was never cloned or a clone has since moved. A project's policy can be overridden by hand (absent fields mean the configured default):

{
  "version": 1,
  "repos": {
    "group/sub/repo": {
      "source": "mirror",          // "mirror" | "local"
      "localPath": null,           // set when source == "local"
      "cloneStrategy": "full",
      "freshnessMode": "manual",   // "ttl" | "always" | "manual"
      "freshnessTtlSeconds": 3600
    }
  }
}

add_repo merges into what is already recorded, so re-pointing a project cannot reset a freshnessMode you set by hand. sync_repo(project) fetches immediately and follows the project's source (a mirror, or the clone's own remote). That is what makes manual usable, and what to call when you want a fresh answer now. Unlike a read, sync_repo reports a failure as an error rather than answering from the cache.

Reading from a clone of your own

gitlab_add_repo(project="group/sub/repo", local_path="C:\\work\\repo")

The path is checked when you call it: it must exist and be a git repository, working clone or bare, so a typo fails at the call that records it. From then on every read runs against that clone. Nothing is cloned, and nothing is written to your working tree, so uncommitted work is untouched. A branch that is not the checked-out one is still found through its remote-tracking ref, because that is where git clone keeps every other branch. A clone with no remote still reads everything it holds; a miss says so plainly instead of pretending the ref does not exist.

gitlab_add_repo(project="group/sub/repo")             # back to a mirror
gitlab_remove_repo(project="group/sub/repo")          # forget it entirely
gitlab_remove_repo(project="group/sub/repo", delete_mirror=True)

remove_repo never deletes a clone: that repository is yours, and the server only ever deletes inside its own mirrors/ directory. Forgetting a mirror-backed project does not stick, because the next read registers it again. Forgetting a local registration does stick, and is how a project goes back to being read from a mirror.

Troubleshooting

A failed read explains itself: the cause, a hint when we recognise the failure, and git's own output as evidence (git said: …). No extra logging is needed.

What you see

What it means

GitLab rejected the credentials

GITLAB_TOKEN is missing, expired, or lacks read_repository access to the project. GitLab reports a project you cannot see as a 401 too, so a typo in the project path looks identical.

GitLab could not find this project

Check the project path (group/subgroup/repo) and the token's access to it.

TLS verification failed

The server uses a private CA; point GITLAB_CA_INFO at the CA bundle.

Could not reach GitLab

Network or VPN.

[refresh failed: …] in a result

The source could not be refreshed, so the answer came from the cache as of synced_at. The note says why; the causes are the same as for a failed read.

The registered clone at … is not a usable git repository

The path recorded by add_repo has been moved, deleted or emptied. Run add_repo again with the current path, or remove_repo to go back to a mirror.

The registered clone at … has no remote to fetch from

The clone has no remote configured, so a ref it does not already hold cannot be fetched. Everything it does hold still reads.

search finds nothing, although you can see the text

Matching is literal and takes one argument: foo|bar means that text, not foo or bar. Pass regex=true for alternation. A plain pathspec's ** needs a directory level (src/**/*.tsx misses src/index.tsx; use :(glob)src/**/*.tsx).

pickaxe lists no commits for text you can see

It reports commits whose diff changed how many times the text appears (git log -S), so text unchanged since the root commit, or brought in by a merge git does not diff, has no commit to list. Matching is literal and case-insensitive; case_sensitive=true follows the case exactly.

A link is refused: URL host "…" does not match GITLAB_HOST ("…")

The link belongs to another GitLab instance. Use a link from the instance GITLAB_HOST names; the server will not read a same-named project from elsewhere.

A link is refused: is a file link; use get_file for it (or directory, commit, history)

The link went to a tool that does not read it. Each kind has one reader: blob/raw/blame → get_file (or blame), tree → list_tree, /-/commit/ → get_commit, /-/commits/ → log. parse_gitlab_url says what a link names if it is unclear.

A link is refused: is a merge request link

Merge requests, issues and other non-code links name no version-control object, so they are refused by name. Take the ref and path from the page and call the tool directly.

A context call is refused

It needs output="matches"; files and count have no matched line to put context around.

list_repos prints clone_strategy=blobless(unsupported)

A repos.json entry asks for a clone strategy this server does not implement, so every read of that project fails with the value named. Set "cloneStrategy": "full" if the data on disk is a full clone, or remove_repo it with deleteMirror=true and use the project again, which clones it in full.

The credential case is worth one extra note: a running MCP server keeps the token it started with. Editing GITLAB_TOKEN does nothing to a server that is already running, which makes a valid token look broken. Restart the client (for OpenCode, opencode service restart) after changing it.

The first read of a project also clones it, so it is slow exactly once.

Security

  • The token is only ever passed as -c http.extraheader=… on the git command line, so it never lands in .git/config.

  • The token is redacted from error output before it reaches a tool result.

  • credential.helper= is cleared and GIT_TERMINAL_PROMPT=0 is set, so a bad token fails fast instead of prompting or hanging.

  • Refs and paths beginning with - are rejected to prevent argument injection.

  • Git is invoked with an argument array and shell: false.

  • SECURITY.md has the longer account: what the token can reach, what the server writes to disk, and the risks it cannot remove.

Read more

  • How it works: where content comes from, freshness, and the read-only guarantee.

  • Development: build, test, releasing and the source layout.

  • Plan: the design decisions and what is next.

  • History: milestones, findings, and the evidence from live checks.

Available Tools

16 tools
add_repoRegister a repositoryA

Register a project with this server. With local_path, reads use that existing clone instead of a server-side mirror; the path is checked to be a git repository here, so a wrong path is refused now rather than at read time. Without it, the project is registered as mirror-backed, which also makes it appear in list_repos before it has ever been read. Nothing is cloned or fetched: use sync_repo for that, or simply read the project. A project already registered is updated; its freshness policy is kept.

Inputs: project, local_path.

blameBlame a file at a refA
Read-only

Show who last touched each line of a file at a branch, tag, or commit — git blame --porcelain — without a working tree. Lines are grouped into runs from the same commit: a row is commit, author, date, line range, then the lines themselves, numbered. lines narrows the blame to a range ("118-126", or "118"), and a range past the end is clamped and reported rather than treated as an error. max_lines caps how many lines come back. The ref defaults to the repository's default branch, and the result states the commit it blamed and synced_at. A binary file is refused: git blames one, but its lines carry NUL bytes. Give either url — a GitLab /-/blame/<ref>/<path> link, whose #L12-20 anchor narrows the blame — or ref and path; the link's host must be GITLAB_HOST.

Inputs: ref, url, path, lines, +2 more.

compareCompare two refsA
Read-only

Summarise what changed on to since it split from from — git diff <merge-base> <to> — as one row per file: git's status letter (A, M, D, R091 old -> new) and ± counts, or binary. The merge_base is printed, so which diff ran is never hidden; merge_base: false asks for the raw two-dot diff instead, and when the two refs share no history the two-dot diff runs with merge_base="none". This never returns a patch — a branch diff is unbounded — so the file list is capped by max_files and a capped answer says so. Use diff_file for one file's patch, passing a printed merge_base as from for the branch view of it. path is a git pathspec, and it narrows rename detection with it: a rename can then appear as a delete and an add. Both refs are resolved before the diff and both commits are stated in the result, with synced_at.

Inputs: to (required), from (required), path, project, +2 more.

diff_fileDiff one file between two refsA
Read-only

The unified diff of one file between two refs — git diff -M <from> <to> -- <path> — without a working tree. The caller names the file and the diff is scoped before git runs, so nothing is found by parsing a patch header. context sets the lines shown around each hunk (default 3, 0-10) and max_bytes caps the diff. Scoping by path narrows rename detection, so a renamed file is read as an added (or deleted) file. The two refs are a raw two-dot diff: pass a merge_base printed by compare as from for the branch view of one file. A file that is unchanged between the refs yields no diff, and one absent from both is reported as not found. A binary file is refused: git has only a "Binary files … differ" line for it, which is not a patch.

Inputs: to (required), from (required), path (required), context, +2 more.

get_commitRead one commitA
Read-only

Read one commit — its sha, parents, author and committer with dates, and full message — then the files it changed with git's status letter (A, M, D, R091 old -> new) and their line counts, or binary. rev accepts a full or abbreviated sha, or a branch or tag. patch: true appends the unified diff, capped by max_bytes. A merge commit reports merge="true" and no file list, because git shows no diff for a merge by default: compare a parent with the commit to see what it brought in. The result states the commit it resolved rev to and synced_at. Give either url — a GitLab /-/commit/<sha> link — or rev; the link's host must be GITLAB_HOST.

Inputs: rev, url, patch, project, +1 more.

get_fileRead a file at a specific refA
Read-only

Return the contents of a file at a given branch, tag, or commit WITHOUT checking it out. Use this to inspect files on a branch other than the one currently checked out. Give either url — a GitLab web link, whose #L12-20 line anchor is honoured — or both ref and path. The repository is mirrored locally on first use and refreshed when it goes stale, or, for a project registered with add_repo, read from your own clone. The result reports the resolved commit and the last sync time. If the copy cannot be refreshed, the cached copy answers and the result says the refresh failed.

Inputs: ref, url, path, project, +1 more.

list_refsList branches and tagsA
Read-only

List branch and/or tag names in a repository, without cloning it into a working tree. Each row is kind, short commit, name, and the tip commit's subject. The list reflects the local copy as of synced_at (by default at most 10 minutes old); a ref created since that moment may be missing, because a stale list cannot be detected. Use resolve_ref to check one specific ref, which does refresh on a miss. If the copy cannot be refreshed, the cached copy answers and the result says the refresh failed.

Inputs: kind, search, project, max_refs.

list_reposList known repositoriesA
Read-only

List the repositories this server knows about, with the freshness policy applied to each and when it last synced successfully. Each row is the project path followed by source (mirror or local), freshness (ttl, always or manual), ttl, clone_strategy=<value>(unsupported) when the registration names a strategy this server does not implement (only full is; such a project refuses to be read until it is re-cloned), present (whether the data is on disk), local_path for a local clone, and synced_at, or unknown when it has never synced. A mirror-backed project is registered the first time it is used; add_repo registers one before that, and is also how a project is pointed at an existing local clone. freshness=manual means nothing refreshes it by itself: use sync_repo.

Inputs: no named parameters.

list_treeList a directory at a refA
Read-only

List the files and subdirectories at a given ref, one level deep by default; set recursive to walk the whole subtree. Files carry their size in bytes, so you can tell a small config from a bundle before reading it. Names are full paths relative to the repository root. Give either url — a GitLab tree link — or ref (and optionally path). Everything comes from git, so no file is fetched to answer this. If the copy cannot be refreshed, the cached copy answers and the result says the refresh failed.

Inputs: ref, url, path, project, +2 more.

logList commits at a refA
Read-only

List commits on a branch, tag, or commit — git log — newest first, without cloning it into a working tree. Each row is commit, date, author, subject, and a merge commit carries (merge). The ref defaults to the repository's default branch, is resolved before the walk, and the result states the commit it walked and synced_at. path is a git pathspec, so globs work — and history stops at a rename, because git's --follow accepts only one path and walks renames backwards; pass the new name or use get_commit when you need to see the rename itself. limit bounds the answer; when it hides older commits the result says so rather than inventing a total. Use get_commit on a row's full sha to see what it changed. Give either url — a GitLab /-/commits/<ref>[/<path>] link, e.g. 'https://gitlab.example.com/group/repo/-/commits/fix/PROJ-7608/src/index.tsx' — or ref and path; the link's host must be GITLAB_HOST.

Inputs: ref, url, path, limit, +1 more.

parse_gitlab_urlParse a GitLab web URLA
Read-only

Turn a GitLab web link into the project, ref, path and line range a read needs — including a ref that contains slashes, which is resolved against the project's own refs. Accepts /-/blob/, /-/raw/, /-/tree/ and /-/blame/ links, which name a file or a directory, and /-/commit/ and /-/commits/[/], which name a commit and a history; merge request and other links are refused by name rather than guessed at. The link's host must be GITLAB_HOST. Resolving may mirror the project on first use; set resolve: false to parse offline without touching it. get_file, list_tree, log, get_commit and blame accept a url directly, so this tool is for inspecting a link, not for reading.

Inputs: url (required), resolve.

pickaxeFind commits that changed a stringA
Read-only

Find the commits whose diff changed how often a piece of text appears — git log -S — so you can answer "when was this introduced, and by whom?". Each row is commit, date, author, subject, most recent first. The text is matched literally and case-insensitively, as search matches it, so a pickaxe never reports "nothing changed" about text the caller can see. Unlike search this walks the history of the ref and is therefore slower; pair it with search to go from "where is it" to "when did it appear". The ref defaults to the repository's default branch, and path is a git pathspec.

Inputs: query (required), ref, path, limit, +2 more.

remove_repoForget a repositoryA
Destructive

Forget a project's registration. With delete_mirror, the server-side mirror is deleted as well — but only a mirror: a registered local clone is never touched, and the request is reported as ignored. Forgetting a mirror-backed project is not permanent, since the next read registers it again and clones it again if the mirror was deleted. Forgetting a local registration is how a project goes back to being read from a mirror.

Inputs: project, delete_mirror.

resolve_refResolve a ref to a commitA
Read-only

Resolve a branch, tag, or revision to the full commit SHA it points at, and say what it is (branch, tag, or commit for anything else). Use this to confirm a ref exists before reading files from it. If the ref is missing, the error lists similar names, and it says so when the copy could not be refreshed, since then a missing ref proves nothing.

Inputs: ref (required), project.

sync_repoFetch a repository nowA

Fetch a project immediately, ignoring its freshness policy — the way to refresh a project whose policy is manual, and to check for new refs without waiting for the TTL. A mirror-backed project is cloned if its mirror is missing; a project registered as a local clone is fetched from that clone's own remote. Unlike a read, a failure is reported as an error rather than answered from the cached copy, because this call exists to fetch.

Inputs: project.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 16 tools

    Disambiguation5/5

    Every tool targets a distinct git/GitLab operation, and the descriptions actively disambiguate the risky pairs: `compare` explicitly never returns a patch and points to `diff_file`; `search` points to `pickaxe` for 'when' vs 'where'; `log`/`get_commit`/`blame` each own a different history view. Repo lifecycle tools (add/remove/sync/list_repos) and ref tools (list_refs/resolve_ref) also have clear, non-overlapping roles.

    Naming Consistency4/5

    The dominant pattern is verb_noun (get_file, list_tree, get_commit, list_refs, list_repos, add_repo, remove_repo, sync_repo, resolve_ref, diff_file, parse_gitlab_url), which is predictable. The deviations (log, blame, compare, search, pickaxe) are deliberate git-subcommand parity, so they are still guessable, but it is a mixed convention rather than a single one.

    Tool Count4/5

    16 tools is at the upper end of the comfortable range, but for a read-only git surface each one maps to a real, non-redundant operation (files, refs, history, diffing, repo registration). Nothing feels padded, though a few could arguably be folded (e.g. resolve_ref into list_refs).

    Completeness4/5

    The read-only domain is well covered end to end: registration/sync, ref discovery and resolution, tree/file reads, grep, pickaxe, log, commit, blame, and both branch-level and file-level diffs. Minor gaps remain — no way to discover projects on the GitLab host without knowing the path, and no merge request/issue/CI reads (explicitly refused), which limits use as a general GitLab API client.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      Enables AI-powered exploration and interaction with GitLab instances through comprehensive search, code browsing, and repository management. Supports both self-hosted and GitLab.com with flexible authentication for read and write operations.
      69
      412 npm
      13
      MIT
    • F
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables AI agents to interact with GitLab repositories, monitor documentation changes, manage issues and merge requests, and search across projects with comprehensive file and commit operations.
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to analyze local Git repositories with commit logs, diffs, blame, statistics, and branch analysis using native git commands without requiring a GitHub API.
      66 npm
      MIT