npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mohou/gitlab-mcp

v1.0.1

Published

MCP server and CLI for a self-hosted GitLab CE 11.3 instance - merge request workflow: create, review, line-level comments, resolve, merge

Readme

@mohou/gitlab-mcp

An MCP server and a CLI for a self-hosted GitLab CE 11.3.0 instance, built around the merge-request workflow: create, read the diff, comment on a line, reply, resolve, merge.

Two front ends, one core. The CLI has no tool list of its own - its subcommands and flags are derived from the same Zod schemas the MCP tools declare, so the two cannot drift.

Why this exists instead of glab or an existing MCP server

GitLab 11.3.0 was released in September 2018. Every maintained tool has moved on:

| Tool | Why it does not work here | | --- | --- | | glab (official CLI) | Officially supports GitLab 16.0+; 15.x and earlier are explicitly unsupported | | GitLab's official MCP server | Requires GitLab 18.3+ (/api/v4/mcp and OAuth dynamic client registration) | | Community GitLab MCP servers | Written against the current API and GraphQL; against 11.3 they 404 or silently misbehave |

The REST API v4 does exist on 11.3 and is complete enough for review work. What it lacks is everything newer, and this client is written against the 11.3 API documentation rather than against today's docs - see What 11.3 does not have.

Requirements

  • Node.js 22.6+ (the sources are TypeScript, run directly by Node's type stripping - there is no build step)
  • A GitLab personal access token with the api scope

Install

# as a dependency of something else
npm install @mohou/gitlab-mcp

# the MCP client launches the server through npx, so nothing must be installed first:
npx -y @mohou/gitlab-mcp

# the CLI, without installing anything globally:
npx -y --package @mohou/gitlab-mcp mohou-gitlab tools

The package ships two binaries: gitlab-mcp (the MCP server, named to match the package so that npx @mohou/gitlab-mcp finds it) and mohou-gitlab (the CLI).

From a source checkout - there is no build step, Node runs the TypeScript directly:

cd gitlab-mcp
npm install
npm test          # 89 offline tests against a mock GitLab 11.3

Configure

| Variable | Meaning | | --- | --- | | GITLAB_BASE_URL | instance root, e.g. http://gitlab.example.com (not /api/v4) | | GITLAB_TOKEN | personal access token, sent as the PRIVATE-TOKEN header | | GITLAB_READ_ONLY | true registers only read-only tools, and the CLI refuses write subcommands | | GITLAB_TIMEOUT_MS | request timeout, default 30000 | | GITLAB_MAX_RETRIES | retries for 429/502/503/504, default 2 | | GITLAB_TLS_REJECT_UNAUTHORIZED | false for a self-signed certificate |

The token must be treated as a write credential: read_api does not exist before GitLab 12.10, so api is the narrowest scope that can read the API at all. Use a dedicated account, not a personal one.

Registering the MCP server

Any MCP client works; the server speaks stdio.

{
  "mcpServers": {
    "gitlab": {
      "command": "npx",
      "args": ["-y", "@mohou/gitlab-mcp"],
      "env": {
        "GITLAB_BASE_URL": "http://gitlab.example.com",
        "GITLAB_TOKEN": "glpat-xxxxxxxxxxxx"
      }
    }
  }
}

From a source checkout, point command at node and args at the absolute path to bin/gitlab-server.mjs instead.

In this harness (dsh), register it with the mini-app MCP tool instead of editing a file:

mini_app_mcp_add id=gitlab command=node args=["/path/gitlab-mcp/bin/gitlab-server.mjs"] env={GITLAB_BASE_URL:..., GITLAB_TOKEN:...}

Two things to know about that path:

  • ${credential:NAME} / ${env:NAME} placeholders are resolved for the one-off check but not for the live MCP client until the host restarts. Pass literal values, or register the row and restart once, then verify with mini_app_mcp_list.
  • The harness reaches the host over VPN. If the VPN is down, the first call fails with network error: fetch failed - is the VPN connected and the host reachable?.

CLI

export GITLAB_BASE_URL=http://gitlab.example.com
export GITLAB_TOKEN=glpat-xxxx

# installed: `mohou-gitlab ...`   |   without installing: `npx -y --package @mohou/gitlab-mcp mohou-gitlab ...`
node bin/gitlab.mjs tools                       # every tool, read-only ones marked
node bin/gitlab.mjs describe mr_comment_on_line # purpose and flags
node bin/gitlab.mjs whoami                      # proves base URL + token
node bin/gitlab.mjs mr_list --project group/app --state opened --per-page 10
node bin/gitlab.mjs mr_changes --project group/app --iid 12 --max-patch-chars 2000
node bin/gitlab.mjs mr_comment_on_line --project group/app --iid 12 \
    --path src/app.ts --line 42 --body "this branch is unreachable"
node bin/gitlab.mjs raw_api --method GET --path /projects/group%2Fapp/merge_requests
  • Tool names may be given in full or without the gitlab_ prefix (mr_list = gitlab_mr_list).
  • Flags are dashed and accept the schema spelling too: --source-branch and --source_branch are the same flag. --no-include-system sets a boolean false.
  • Output is JSON on stdout, diagnostics on stderr. Exit codes: 0 ok, 1 the call failed, 2 bad usage.
  • --host, --token, --token-file override the environment for one call (the token is removed from argv, so it does not appear in ps); --dry-run prints the request instead of sending it.

A review, end to end

gitlab_mr_list      {project, state:"opened"}                  # which MR
gitlab_mr_get       {project, iid}                             # state, merge_status, work_in_progress
gitlab_mr_changes   {project, iid, maxPatchChars:4000}          # files, counts, capped patches
gitlab_mr_discussions {project, iid, onlyUnresolved:true}       # what is still open
gitlab_mr_comment_on_line {project, iid, path, line, side, body}
gitlab_mr_reply     {project, iid, discussion_id, body}
gitlab_mr_resolve   {project, iid, discussion_id}
gitlab_mr_pipelines {project, iid}                             # the merge gate
gitlab_mr_merge     {project, iid, sha}                        # sha pins the source HEAD

gitlab_mr_comment_on_line is the tool that earns its keep. GitLab does not accept "file + line": a diff note needs the merge request's base/start/head SHAs and the old-side line number. This tool reads the newest diff version, reconstructs the hunk numbering, fills both line numbers in, and refuses locally when the line is not in the diff - naming the ranges that would have worked:

cannot place a comment at src/app.ts:99 (new side). src/app.ts: no commentable new-side line 99
(commentable new-side lines: 10-18). Lines that exist in the diff can be commented on;
use gitlab_mr_changes to see the diff.

Tools

Core tools (code, src/entities/):

| Tool | R/W | Purpose | | --- | --- | --- | | gitlab_whoami | ro | the authenticated user - the cheapest way to prove the token | | gitlab_server_version | ro | which GitLab version is answering | | gitlab_project_get / gitlab_project_search | ro | resolve a project path or id | | gitlab_labels_list / gitlab_users_search | ro | label names, and the numeric user ids this API takes | | gitlab_branches_list / gitlab_file_get | ro | branches; one file at a ref, capped | | gitlab_mr_list / gitlab_mr_get | ro | find and read merge requests | | gitlab_mr_changes | ro | per-file diff summary with capped patches | | gitlab_mr_discussions | ro | threads, with each diff comment's file and line | | gitlab_mr_pipelines / gitlab_mr_versions | ro | the merge gate; diff versions (debugging) | | gitlab_mr_create / gitlab_mr_update | rw | create and edit, including the draft translation | | gitlab_mr_comment | rw | a comment on the merge request itself | | gitlab_mr_comment_on_line | rw | a diff comment on one line | | gitlab_mr_reply / gitlab_mr_resolve | rw | continue a thread; resolve it | | gitlab_mr_merge | rw | merge, with the failure modes explained | | gitlab_raw_api | rw | any endpoint - the escape hatch |

Declared extras (tools.d/gitlab-extras.json, read-only, no code): issues, members, project pipelines, pipeline jobs, commits, tags, group projects. See tools.d/README.md for the declaration DSL and how to add more.

npm run tools:describe -- <filter> prints the contract as the model sees it, asked of the server itself over stdio.

What 11.3 does not have

Every entry here is a trap a tool written against current documentation would fall into.

| Missing | Consequence here | | --- | --- | | Merge request approvals API | There is no approve endpoint on CE 11.3 (doc/api/merge_request_approvals.md does not exist in the 11.3 tree). Review state is carried by diff comments and by resolving threads. | | reviewers attribute | Assignment is the only way to route a merge request; gitlab_mr_list has no reviewer filter. | | draft parameter | A draft merge request is one whose title starts with WIP: , reported back as work_in_progress: true. gitlab_mr_create/gitlab_mr_update take draft and translate it. | | read_api scope | Tokens are all-or-nothing write credentials. | | Global code search | /search has no blobs scope at the top level; code search is per project. | | Keyset pagination | Offset pagination only; every list tool reports pagination.nextPage. | | squash on merge | Squash is set on the merge request when it is created or updated, not at merge time. | | rules/workflow/needs in CI | 12.x features. CI config for this instance must use only/except. | | /projects/:id/users (unverified) | Not used here; user lookup goes through the global /users endpoint. | | GraphQL | Experimental at best on 11.0+; this client is REST v4 only. |

Two error messages exist purely because of this table: a 400 mentioning reviewer_ids, draft or approval rules, and a 401 explaining the scope situation.

Reading the right documentation

Do not read today's API docs when changing this code. Read the version-matched ones from the source tag that produced the instance:

https://gitlab.com/gitlab-org/gitlab-foss/-/raw/v11.3.0/doc/api/merge_requests.md
https://gitlab.com/gitlab-org/gitlab-foss/-/raw/v11.3.0/doc/api/discussions.md

That is where the parameters in this client were checked, including the detail that a diff version reports head_commit_sha / base_commit_sha / start_commit_sha while a comment position wants head_sha / base_sha / start_sha.

Verification

npm test runs against test/mock-gitlab.mjs, which is faithful to the 11.3 shapes (the *_commit_sha naming, hunk numbering, the 405/406/409 merge failures, nested validation errors) rather than to GitLab's behaviour. Only a real instance can settle the rest:

GITLAB_BASE_URL=http://gitlab.example.com GITLAB_TOKEN=... \
  npm run verify:live -- --project <group/app> --iid <mr>          # read-only
GITLAB_BASE_URL=... GITLAB_TOKEN=... \
  npm run verify:live -- --project <group/app> --iid <mr> --write  # + create/delete a diff note

It reports PASS/FAIL/SKIP per assumption and exits non-zero on any failure. In --write mode the only mutation is one probe comment, which it deletes again.

Result against a live GitLab CE 11.3.0 instance (revision 17bd59a): 14 passed, 0 failed, in both modes. What only that run could establish, and did:

  • PRIVATE-TOKEN authentication works on 11.3.
  • A project path arrives correctly percent-encoded.
  • Pagination headers are present and usable (184 merge requests, nextPage=2).
  • GET /versions names its SHAs base_commit_sha / start_commit_sha / head_commit_sha, which is not what a comment position calls them - the mapping the client performs is correct.
  • Real diff text parses, and a real line resolves to a commentable position.
  • A nested position object is accepted when creating a diff note - the single assumption the offline suite could never test, since it was written from the same reading of the docs.
  • The created note comes back carrying its position, appears in the discussions list, toggles through resolve/unresolve, and deletes cleanly.
  • A merged merge request still reports merge_status: can_be_merged, so state is the field that decides whether a merge is possible. The tool descriptions say so.

Still unverified live, and deliberately listed rather than glossed over:

| Area | Why, and how to close it | | --- | --- | | The draft positive case (WIP: title -> work_in_progress: true) | Needs a merge request that is actually a draft; the verified project had none among 100. | | gitlab_mr_create / gitlab_mr_update / gitlab_mr_merge | Needs a throwaway branch in a project where creating a merge request is acceptable; 11.3 has no API to delete a merge request, so the residue is one closed or merged one. |

Everything else in this file that is not one of those two lines has been exercised against the real instance.

Extending

  • A new endpoint with no logic - add a declaration to tools.d/*.json. No code, no restart of anything but the server.
  • A new endpoint with logic, or a non-JSON response - add a tool in src/entities/. Text endpoints (job traces, raw files) cannot be expressed in the JSON layer, because that layer parses every response as JSON.
  • Both front ends pick it up automatically: the MCP tool and the CLI subcommand appear together.

Layout: src/gitlab.ts (transport, pagination, error translation) - src/diff.ts (unified diff → position) - src/entities/ (tools) - src/jsonTools.ts (the declaration DSL) - src/cli.ts + src/args.ts (CLI) - src/index.ts (MCP server).

Maintainer: publishing

Prerequisites, in order:

  1. Node 22.6+ locally, and npm test green (prepublishOnly enforces it).
  2. npm login (or NPM_TOKEN in the environment) for the account that may publish.
  3. The @mohou scope must exist and you must be able to publish to it. A scoped name is not optional: npm rejects mohou/gitlab-mcp, so the package is @mohou/gitlab-mcp. If @mohou is not yet an npm organization, create it on npmjs.com and add yourself as an owner - or, for an internal registry, map the scope instead:
    # .npmrc (safe to commit - no token in it)
    @mohou:registry=https://your-internal-registry/
    That .npmrc also decides where npm publish goes, so set it before publishing.
  4. publishConfig.access = "public" is already set in package.json; without it a scoped package publishes as restricted and fails on a free plan.

Then:

npm test                      # 93 offline tests
npm run pack:check            # the file list that would ship - verify dist/ and tools.d/ are in it
npm run verify:package        # pack, install into a temp dir, run both binaries, speak MCP to them
npm publish --dry-run         # the whole publish path, including prepublishOnly, without publishing
npm version patch             # or minor/major; updates package.json + git tag
npm publish                   # prepublishOnly runs both gates above

Development needs no build; the published package does

This is the one place where the package departs from a plain "run the TypeScript" layout, and the reason is a hard Node restriction:

ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING

Node strips types for the application's own sources but refuses to do so for anything under node_modules. So src/*.ts runs fine in a checkout (and every test relies on that) and cannot run at all once installed as a dependency. The package therefore ships dist/*.js, built by scripts/build.mjs (esbuild, dependencies kept external).

The bin entries resolve the entry source first, bundle second:

bin/gitlab-server.mjs   ->  src/index.ts  if it exists, else dist/index.js
bin/gitlab.mjs          ->  src/cli.ts    if it exists, else dist/cli.js

Sources first, not bundle first, and the order is not cosmetic: preferring dist/ means that after any build a checkout silently runs the stale bundle, which once hid a real bug in the sibling jira-mcp project during exactly this kind of verification. The published tarball contains no src/ at all, so an install takes the bundle path; npm run verify:package fails if src/ ever appears in the tarball, because shipping it would send an install down the source path into ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING.

prepack runs the build, so npm pack and npm publish always emit a fresh bundle; dist/ is gitignored. Editing a tool means editing the TypeScript, and nothing else - tests run the sources and publishing rebuilds.

Other notes specific to this package:

  • tools.d/ must ship. The server loads tools.d/*.json from the package root at startup; if that directory were dropped from files, every JSON-declared tool would disappear and startup would fail. Both pack:check and verify:package check for it.
  • engines requires Node 22.6+ because the fallback path and the test suite use type stripping. The bundle itself would run on older Node.
  • No provenance attestation. npm publish --provenance needs a public source repository on a supported CI provider; this repository is self-hosted, so provenance is not available.
  • Run npm publish --dry-run before the real one. npm exports its configuration to lifecycle scripts, so a dry run reaches prepublishOnly with npm_config_dry_run=true - which once made verify:package fail, because npm pack prints a filename without writing the file and the install that follows hits ENOENT. The script neutralises that variable; the dry run is the check that it stays neutralised.
  • prepublishOnly runs the offline suite and the package verification only. The live checks (npm run verify:live) need VPN access and a token, so they are not part of publishing.