gitlab-mcp-server
AGENTS.md47 starsChanged 6 months agoMITView on GitHub
MCP server in Go that exposes GitLab REST + GraphQL as MCP tools, resources, and prompts. Communicates via stdio (default) or HTTP. Catalog-first tool registration: 181 sub-packages under internal/tools/, with three runtime surfaces (dynamic default, meta, individual). That site, published at <https://docs.jmrp.io/gitlab-mcp-server/>, is the only home of the user documentation; docs/ keeps docs/development, for contributors (ADR-0025). The canonical agents live under .github/agents/ and the skills under .github/skills/; .claude/agents/ and .claude/skills/ hold a symbolic link to each for Claude Code. OpenCode…
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository22 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- llms-full.txtllms.txt
- llms.txtllms.txt
- create-architectural-decision-recordSkill
- create-implementation-planSkill
- create-mcp-evaluationSkill
- create-mcp-toolSkill
- create-specificationSkill
- generate-project-documentationSkill
- generate-release-notesSkill
- git-commitSkill
- golang-patternsSkill
- golang-testingSkill
- go-safe-move-refactorSkill
- go-source-documentationSkill
- increase-test-coverageSkill
- modularize-go-packageSkill
- review-and-refactorSkill
- update-project-documentationSkill
- update-starlight-docsSkill
- upstream-contributionSkill
The file
We split this AGENTS.md into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 36
English-only artifacts: Every file committed to this repo must be in English, including doc comments, ADRs, branches, commits, and MCP tool …
- Very specific: line 150
ActionSpec: add a canonical route in internal/tools/{domain}/action_specs.go with metadata, owner package, compatibility policy, and tests.
- Very specific: line 209
Build tag: all E2E tests are gated by -tags e2e.
- Very specific: line 238
The surface is a subtest, not a family: every scenario runs on dynamic, meta and individual, so -run 'TestIssue_Lifecycle/dynamic' is how …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
gitlab-mcp-server — Agent Quick Reference
MCP server in Go that exposes GitLab REST + GraphQL as MCP tools, resources, and prompts. Communicates via stdio (default) or HTTP. Catalog-first tool registration: 181 sub-packages under
internal/tools/, with three runtime surfaces (dynamicdefault,meta,individual).
Read first
| Where | Why |
CLAUDE.md | Project rules and context, ADR list, agents/skills catalog, docs/development index |
.github/copilot-instructions.md | Auto-loaded by VS Code Copilot; has the language policy, env var table, E2E recipes |
.github/instructions/*.md | Auto-applied coding standards (go, MCP, OWASP, comments, code review) |
docs/development/tool-surfaces-and-action-core.md | Surface ownership and catalog projection rules |
docs/development/adr | Architectural Decision Records (catalog-first is ADR-0004) |
site/src/content/docs/ | User documentation: one page per slug, each with its Spanish twin under es/ |
That site, published at https://docs.jmrp.io/gitlab-mcp-server/, is the only
home of the user documentation; docs/ keeps docs/development, for
contributors (ADR-0025).
The canonical agents live under .github/agents/ and the skills under
.github/skills/; .claude/agents/ and .claude/skills/ hold a symbolic
link to each for Claude Code. OpenCode wiring is per developer: opencode.json and
.opencode/ are git-ignored and never committed.
Hard invariants
- Catalog-first registration. For ordinary GitLab API actions, add or
update domain-local
ActionSpecs and handlers. Do not add package-localRegisterToolsfunctions or ad hocmcp.AddToolcalls. The catalog ininternal/tools/action_catalog.goprojects everything into meta, dynamic,gitlab://tools, audits, LLM files, and individual tool surfaces. (ADR-0004.) - English-only artifacts. Every file committed to this repo must be
in English, including doc comments, ADRs, branches, commits, and MCP
tool descriptions. The exceptions are the Spanish half of the user
documentation: the twin pages under
site/src/content/docs/es/, and the Spanish text the site and the generators that write it keep beside the English (anesfield, alabelEs,site/src/content/i18n/es.json); and the server's Spanish message catalog,internal/i18n/catalog_es.go, with the Spanish the tests compare its renders against (issue 1547), which translates the English catalog key by key. Chat with the developer in any language you want. - Conventional commits.
feat:,fix:,docs:,test:,refactor:,chore:. Use thegit-commitskill (.claude/skills/git-commit/) for auto-detected type/scope. - A copyright and license header in every source file. Each source
file opens with
Copyright (c) 2026 José M. Requena PlensandSPDX-License-Identifier: MITas comments of its language, a Go file with a blank line before its package clause or build constraint.make fix-file-headerswrites them into a new file andmake check-file-headersis the gate; which files carry them and where is indocs/development/file-headers.md. - Coverage target: maximum possible, 100% when feasible. CI fails
below 90% total coverage (
COVERAGE_MINin.github/workflows/ci.yml); in practice the team pushes per-package coverage to 100% unless there is a hard blocker (generated code, third-party interfaces). When you cannot reach 100% on a function, add a documented reason at the call site and prefer table-driven tests that exercise both happy and error paths.
Common commands
# Build
make build # ./dist/gitlab-mcp-server
make build-all # all 6 GOOS/GOARCH targets
# Test (fast)
make test-pkg PKG=branches # one domain — the workhorse
go test ./internal/tools/branches/ -count=1 -v
go test ./internal/tools/ -run TestBranch -count=1
# Test (full)
make test-short # all unit tests, no coverage
make test # all unit tests, with coverage.out
make coverage # writes coverage.html
# Lint / analyze
make analyze # 40 steps: golangci-lint, govulncheck, markdownlint and the cmd/ audits and record checks
make analyze-fix # apply gofumpt/goimports/gci/markdownlint --fix
make golangci-lint # Go-only gate
golangci-lint run --build-tags e2e ./internal/tools/branches/ # one package
# Audit / regenerate
go run ./cmd/audit_surface_quality/
go run ./cmd/audit_tokens/
go run ./cmd/audit_dynamic_aliases/
go run ./cmd/audit_test_names cmd internal test
make audit-godocs # writes dist/analysis/godoc.md
# MCP Inspector
make inspector # builds + launches at http://127.0.0.1:6274
make inspector-stop
golangci-lintruns with--build-tags e2e(set in.golangci.yml). Running it directly without that tag will fail on e2e-tagged files. Usemake golangci-lintor pass--build-tags e2eyourself.
Post-edit regeneration matrix
| You edited | Run |
| Domain tool (added/renamed/changed input or output) | go run ./cmd/audit_tokens/ -footprint |
| ActionSpec metadata (catalog routes) | go run ./cmd/gen_action_catalog_manifest/ (and --check in CI) |
Pipe tables in README.md, docs/ or the site | go run ./cmd/format_md_tables/ (and --check) |
A hand-written page under site/src/content/docs/ | Its Spanish twin under es/ in the same change, then pnpm run i18n:check, facts:check, chips:check, llms:check and version:check from site/ |
| A new site page | Its entry in the sidebar (site/astro.config.mjs) and in SECTIONS (site/scripts/gen-llms.mjs); the generated reference/tools/ need neither |
| Tests, after a test phase | go run ./cmd/gen_testing_docs/ (and --check) |
| Tool surface (registered tools, resources, prompts) | go run ./cmd/gen_llms/ (and --check via make check-llms), plus go run ./cmd/gen_lhm_manifest/ (and --check via make check-lhm-manifest) |
| Catalog actions or groups (the site's tool reference) | make gen-tool-reference (and make check-tool-reference); a new group needs its entry in cmd/gen_tool_reference/domains.json |
server.json | make check-server-json (uses MCP publisher) |
make audit-docs runs the combined documentation gate locally. CI does not
run that target, but it runs its parts in separate jobs: the generated job
runs the Go freshness gates (check-llms, check-tool-reference,
check-lhm-manifest, check-site-stats, check-testing-docs and
check-md-tables among them, beside check-server-json, check-openplugin,
check-footprint and check-action-catalog-manifest), the analyze-md job
runs markdownlint over every .md and .mdx file and the two gates that need
Node (check-doc-links, check-mcpb), site-lint runs the site's static
checks and site-build builds the site and checks what it wrote. Adding a new
check-* target means adding it to one of those jobs. The README statistics, since removed, and
check-footprint sat stale on main for several releases precisely because
nothing gated them, and
docs/development/testing/testing.md drifted for the same reason.
check-testing-docs gates everything in that file a checkout determines and
not the coverage columns, which no two machines agree on; refresh those with
make gen-testing-docs.
The godoc gate, make check-godocs, runs in the generated job too. Two
parts of audit-docs run in no CI job and are yours to run before pushing:
the dynamic alias audit (go run ./cmd/audit_dynamic_aliases/) and the
surface-quality report (go run ./cmd/audit_surface_quality/ -view=all; CI
runs its gate, make check-surface-quality).
Adding a new GitLab API tool
For a full walkthrough use the create-mcp-tool skill
(.claude/skills/create-mcp-tool/). Short version:
- Plan/spec:
@plan-expertagent orcreate-implementation-planskill for non-trivial work. - Sub-package:
internal/tools/{domain}/{domain}.gowith typedInput/Outputstructs (jsonschematags). No domain prefix on types — the package is the namespace. - ActionSpec: add a canonical route in
internal/tools/{domain}/action_specs.gowith metadata, owner package, compatibility policy, and tests. - Handler + tests:
httptest-based table-driven tests usingtestutil.NewTestClientandtestutil.RespondJSON. A GraphQL document sent through that client is validated against the pinned GitLab schema before the mock answers, so a mock can no longer accept what GitLab refuses; a document no test drives is covered bymake check-graphql-documents. That client also records the request each call issues, so a new endpoint shows up indocs/development/request-inventory.jsonaftermake gen-request-inventory. A package no test drives records nothing, andmake audit-1to1-pathsfails on that: it is the one check that reads the request rather than the surface. - Markdown formatter: register via
toolutil.RegisterMarkdown[T](fn)in the sub-packagemarkdown.goinit(). List formatters must addtoolutil.HintPreserveLinksas the first hint inWriteHints(). A type added or removed moves the registry figuresCLAUDE.mdstates, andTestAllMarkdownFormattersRegisterednames the sentence to write. - Refresh:
audit_tokens -footprint,audit_metrics -site-stats(make gen-site-stats),gen_action_grants(make gen-action-grants),gen_action_catalog_manifest,format_md_tables,gen_testing_docs,gen_llms,gen_lhm_manifest(run--checkon each before pushing). - Verify:
make test-pkg PKG={domain}andgolangci-lint run --build-tags e2e ./internal/tools/{domain}/. - Document:
make gen-tool-referenceregenerates the site's per-domain tool reference from the catalog (make update-allruns it withgen-site-statsandgen-action-grants); a new catalog group needs its overview and sample questions, in English and Spanish, incmd/gen_tool_reference/domains.json. A change a user can see also updates the site page that describes it and that page's Spanish twin;docs/holds contributor documentation only.
Error handling in tool handlers
The four wrappers live in internal/toolutil/errors.go and NotFoundResult
in internal/toolutil/not_found.go (ADR-0007):
WrapErr(op, err)— read-only ops, generic classificationWrapErrWithMessage(op, err)— mutating ops, includes GitLab messageWrapErrWithHint(op, err, hint)— when a corrective action is knownWrapErrWithStatusHint(op, err, code, hint)— hint only applies on a specific HTTP status; falls through toWrapErrWithMessageotherwiseNotFoundResult(resource, id, hints...)— forIsHTTPStatus(err, 404)on get handlers; returns a structured result with hints at INFO level
For get handlers: check IsHTTPStatus(err, 404) before LogToolCallAll
and return NotFoundResult with nil error. IsHTTPStatus and
ContainsAny come first; status-specific hints come last.
A hint that names a role, a license or an owner is keyed on
IsPermissionRefusal(err), not on a status: GitLab refuses a missing
permission with 401 at many routes (entry 55 of
docs/development/upstream-bugs.md), and a hint keyed on 401 alone would
follow the verdict that the token itself was refused. It is true for a REST
401 or 403 whose body carries no RFC 6750 error code and does not refuse the
account itself (a blocked or deactivated account, for one); a route whose 403
means something else pairs it with IsHTTPStatus.
Markdown formatter pattern
Sub-packages self-register formatters via init() against a type-keyed
registry in internal/toolutil/md_registry.go. There is no central
dispatch. internal/tools/markdown.go is a thin delegator to
toolutil.MarkdownForResult. List output should include
toolutil.HintPreserveLinks so LLMs keep [text](url) clickable.
E2E test gotchas
- Build tag: all E2E tests are gated by
-tags e2e. The unit test suite must still compile when that tag is set. - Compile-only check (no GitLab required):
go test -tags e2e -c -o /dev/null ./test/e2e/gitlab/... - Self-hosted mode reads
GITLAB_URL+GITLAB_TOKENfrom.env. Tests create and delete real resources; the user must have permission.make test-e2e(an alias ofmake test-e2e-gitlab) runs it, and a package the instance cannot serve skips. Docker mode needs the memory for GitLab (it peaks at about 7.5 GiB in a complete CE run and is capped at 12 GiB) and runs GitLab CE + runner + fixture service:
make test-e2e-ce # provisions the stack, runs it, tears it downPipeline/Job tools only work in Docker mode (CI runner required).
Orbit live tests are a separate package at
test/e2e/orbit/with theorbitlivetag. They hit realhttps://gitlab.com/api/v4/orbit/*withGITLAB_COM_TOKENfrom.env(default namespacejmrp-oss/orbit-fixtures):make test-e2e-gitlab-com # full orchestration make test-e2e-gitlab-com ORBIT_FIXTURES_NAMESPACE=acme # other namespaceSee
docs/development/orbit-fixtures.mdfor the fixture layout and indexer caveat.- The surface is a subtest, not a family: every scenario runs on
dynamic, meta and individual, so
-run 'TestIssue_Lifecycle/dynamic'is how one surface is driven on its own.
Release process
make release— GoReleaser snapshot, flattensdist/to GitHub asset names.- Release link names must be exact filenames (e.g.
checksums.txt.sigstore.json,gitlab-mcp-server-linux-amd64). Never add descriptive suffixes like(GPG signature)— the Homebrew formula, winget, the installers andscripts/fetch-release-assets.shlook assets up by exact name and will not find a decorated one. - Push the tag; CI publishes the GitHub Release and the Docker image.
- The public hosted endpoint
https://mcp.jmrp.io/gitlabis deployed out of band from the mcp.jmrp.io host — this repository publishes artifacts only.
Common traps
- Tool surface default is
dynamic(2 tools). Most users expect meta-tools; remind them to setGITLAB_MCP_TOOL_SURFACE=meta(stdio) or--tool-surface=meta(HTTP) when they want the 34/40/51/52-tool catalog (Free/CE, Premium, self-managed Ultimate, GitLab.com Ultimate). .tmp-*/directories at the repository root are git-ignored working dirs (scripts/setup-orbit-fixtures.shclones into.tmp-orbit-*and removes each when it is done). Safe to ignore.- Release asset names are looked up verbatim — see the release section above.
- Coverage minimum 90% (total) is enforced in CI; locally
go test -count=1 -coverpkg=./cmd/...,./internal/... -coverprofile=coverage.out ./cmd/... ./internal/...thengo tool cover -func=coverage.outreports the total the same way. - HTTP mode without
--gitlab-url(orGITLAB_URLin the environment) refuses to start unless--allow-any-gitlab-urlis passed, which only a loopback address or a unix socket accepts. With several instances published, or none under that flag, every client request must sendGITLAB-URL.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

