Thank you for your interest in contributing to gitlab-mcp-server! This guide covers the process for submitting changes, reporting issues, and following project conventions.
By participating, you agree to abide by the Code of Conduct. For security issues, please follow the Security Policy instead of opening a public issue.
- Getting Started
- Development Setup
- Branch Naming
- Commit Messages
- Pull Requests
- Code Standards
- Testing
- Documentation
- Issue Reporting
- Labels
- Clone the repository
- Create a
.envfile with your GitLab credentials (see Configuration) - Run
make buildto verify the setup - Run
make testto ensure all tests pass
- Go 1.27+ — Download
- Node.js 22+ with Corepack — required for the documentation site and MCP Inspector. The site declares
pnpm@11.8.0; pnpm settings live insite/pnpm-workspace.yaml. - Git — configured with push access
- GitLab instance — with a Personal Access Token (
apiscope)
# Build
make build
# Run all tests
make test
# Run tests with race detector
make test-race
# Run end-to-end tests (requires .env with real GitLab credentials)
make test-e2e
# Run end-to-end tests in Docker mode (ephemeral GitLab CE, ~4 GB RAM)
make test-e2e-docker
# Check test coverage
make coverage
# Lint
make lint
# Launch MCP Inspector (interactive tool testing UI)
make inspector
# Stop MCP Inspector
make inspector-stopUse the following naming convention for branches:
| Prefix | Purpose | Example |
|---|---|---|
feature/ |
New functionality | feature/gitlab-wiki-tools |
fix/ |
Bug fixes | fix/pagination-off-by-one |
docs/ |
Documentation only | docs/add-wiki-reference |
test/ |
Test additions | test/increase-mr-coverage |
refactor/ |
Code restructuring | refactor/extract-pagination |
chore/ |
Build, CI, dependencies | chore/upgrade-go-sdk |
Follow Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
| Type | Description |
|---|---|
feat |
New feature or tool |
fix |
Bug fix |
docs |
Documentation changes |
test |
Adding or updating tests |
refactor |
Code change that neither fixes a bug nor adds a feature |
chore |
Build process, CI, dependency updates |
perf |
Performance improvement |
style |
Code formatting (no logic change) |
Use the package name as scope when applicable:
feat(tools): add gitlab_wiki_page_create tool
fix(config): handle empty GITLAB_URL gracefully
test(branches): increase coverage to 90%
docs(readme): update tool count after wiki tools
- Code compiles:
go build ./... - All tests pass:
go test ./... -count=1 - Static analysis is clean:
make analyze(runmake analyze-fixfirst to apply supported Go and Markdown fixes) - New tools have tests with >80% coverage
- Documentation is updated if public API changed
- Commit messages follow conventional commits
- Create a feature branch from
main - Make your changes in small, focused commits
- Push the branch and open a pull request — reviewers are auto-requested via CODEOWNERS
- Fill in the PR template (auto-populated from
.github/pull_request_template.md) - Address review feedback
- Squash-merge once approved (the only allowed merge strategy)
- Small (<200 lines): Preferred — faster review, fewer conflicts
- Medium (200–500 lines): Acceptable for feature additions
- Large (>500 lines): Split into smaller PRs when possible
- Follow idiomatic Go and the repository's consolidated
golangci-lintconfiguration (goimports,gofumpt,gci,govet,staticcheck,gosec, and related checks) - All exported types and functions must have doc comments
- Error wrapping with
fmt.Errorf("context: %w", err) - Use
context.Contextconsistently for cancellation/timeouts - Table-driven tests with
t.Run()subtests
- Each GitLab operation = one canonical action with typed input/output structs
- Use
jsonschemastruct tags for tool input documentation - Register runtime surfaces from the canonical action catalog
- Set appropriate annotations (readOnlyHint, destructiveHint, etc.)
- Return both structured JSON and human-readable Markdown
internal/tools/
├── register.go # RegisterAll() — projects individual tools from the canonical catalog
├── register_meta.go # RegisterAllMeta() — meta-tool registration
├── meta_tool.go # Meta-tool registration infrastructure
├── pagination.go # Pagination type aliases
├── errors.go # Error helpers (bridge to toolutil)
├── markdown.go # Markdown formatting (bridge to toolutil)
├── logging.go # Tool call logging (bridge to toolutil)
└── <domain>/ # 176 domain sub-packages
├── action_specs.go # Canonical ActionSpecs for catalog-backed tool surfaces
├── <domain>.go # Typed input/output structs + handlers
├── <domain>_test.go # Table-driven unit tests
└── markdown.go # Markdown formatters (self-registered via init())
- Unit tests for every tool handler — use
httptestto mock GitLab API responses - Table-driven tests with
t.Run()subtests - Test naming:
TestToolName_Scenario_ExpectedResult - Coverage target: >80% on tool handlers
- No external dependencies: Unit tests must not call real GitLab APIs
# All unit tests
go test ./... -count=1
# Specific package
go test ./internal/tools/... -count=1 -v
# With coverage
go test ./internal/tools/... -coverprofile=cover.out
go tool cover -func=cover.out
# E2E tests (requires real GitLab)
go test -tags e2e -timeout 300s ./test/e2e/suite/This project ships with 7 AI agents and 18 skills for GitHub Copilot and compatible assistants. Key workflows for contributors:
- Adding new tools: Use the
create-mcp-toolskill — it scaffolds the full tool lifecycle (struct, handler, registration, tests, docs). - Improving test coverage: Use the
increase-test-coverageskill to identify gaps and reach the 80% coverage target. - Code quality reviews: Use the
review-and-refactorskill for code quality + OWASP security + MCP pattern checks.
See AGENTS.md for the complete catalog of agents, skills, and instruction files.
Tool definitions are snapshot-tested to detect unintentional changes. Golden files live in internal/tools/testdata/:
tools_individual.json— all individual tool definitionstools_meta.json— all meta-tool definitions
When you intentionally change a tool definition (name, description, schema, annotations), update the golden files:
UPDATE_TOOLSNAPS=true go test ./internal/tools/ -run TestToolSnapshots -count=1Then commit the updated golden files alongside your code changes. The CI will fail if snapshots are out of date.
- Adding a new tool → update the relevant
docs/tools/<domain>.mdanddocs/reference/tools/README.md - Adding a new meta-tool action → update
docs/concepts/meta-tools.md - Adding a new resource → update
docs/reference/resources.md - Adding a new prompt → update
docs/reference/prompts.md - Adding a new capability → update
docs/reference/capabilities/README.md - Changing configuration → update
docs/reference/configuration.md - Adding or modifying tests → update
docs/development/testing/testing.mdwith new test counts and coverage values
All project artifacts must be written in English:
- Source code, comments, doc comments
- Commit messages, branch names
- Documentation, ADRs, specs
- MCP tool names, descriptions, error messages
- Test names and assertions
When creating a new release and uploading binaries to GitHub Releases:
- Build cross-platform binaries with
make release(uses GoReleaser locally, flattensdist/to match GitHub Release asset names) - Create a GitHub release with the new tag and upload the binaries + checksum
Open an issue at https://github.com/jmrplens/gitlab-mcp-server/issues/new/choose and pick a template:
- Bug Report — reproducible defects
- Feature Request — new functionality / new MCP tool
- Enhancement — improvement to existing behavior
- Documentation — missing, outdated or incorrect docs
For security issues, do not open a public issue — report privately via GitHub Security Advisories (see SECURITY.md).
Templates auto-apply the relevant labels listed in Labels.
Labels arrive four ways, none of which removes one placed by hand. Issue templates apply their kind label on submission, and their "Area" dropdown is read by .github/workflows/issue-area.yml, which applies the area label the answer names. Pull requests are labeled from the paths they touch by .github/workflows/labeler.yml against the map in .github/labeler.yml, and from the title's conventional prefix by the same workflow: fix: is a bug, feat: a feature, and nothing else is mapped. Milestones pair with a version label both ways (v2.8.0 for milestone 2.8.0), kept in step by .github/workflows/version-labels.yml. The repo uses a flat label set (no type::/priority:: namespaces — those are GitLab conventions), and the area labels use the same words on issues and on pull requests so the two can be filtered together:
| Label | Color | Used by |
|---|---|---|
bug |
#d73a4a |
Bug Report template; fix: titles |
feature |
#a2eeef |
Feature Request template; feat: titles |
enhancement |
#a2eeef |
Enhancement template (GitHub default) |
documentation |
#0075ca |
Documentation template; path labeler on docs-only PRs |
security |
#d73a4a |
Security Advisories; path labeler on security paths; Area "Authentication" |
ci |
#bfdadc |
Path labeler — pipeline, workflows, lint configuration |
distribution |
#fbca04 |
Path labeler — release artifacts and install channels; Area "Installation" |
dependencies |
#0052cc |
Path labeler and Dependabot — dependency files |
transport |
#5c8dd6 |
Path labeler — cmd/server, the transport e2e modules; Area "Transport" |
mcp |
#a371f7 |
Path labeler — resources, prompts, completions, subscriptions; Area "MCP" |
tools |
#1f883d |
Area "GitLab tools" only: two thirds of PRs touch internal/tools/, so no path |
telemetry |
#6fbf9a |
Path labeler — internal/telemetry, internal/mcpotel; Area "Telemetry" |
site |
#f0a35c |
Path labeler — the site's build and components, not its content; Area "Site" |
tooling |
#9aa5b1 |
Path labeler — the auditors and generators under cmd/; Area "CI and tooling" |
platform |
#e9c46a |
Path labeler — *_windows.go, *_darwin.go, *_unix.go; Area "Platform" |
e2e |
#8bd3c7 |
Path labeler — the e2e suite and its Docker fixtures |
release |
#0e8a16 |
Manual — release tracking issues |
high-priority |
#b60205 |
Manual — critical bugs and security advisories |
needs-triage |
#c2e0c6 |
All issue templates; retired by triage.yml on milestone or close |
v<version> |
#1d76db |
version-labels.yml — paired with the milestone of the same number |
good first issue |
#7057ff |
Manual — newcomer-friendly issues |
help wanted |
#008672 |
Manual — community contributions welcome |
question |
#d876e3 |
Manual — questions / discussions |
duplicate |
#cfd3d7 |
Manual — duplicates of existing issues |
invalid |
#e4e669 |
Manual — out of scope |
wontfix |
#ffffff |
Manual — accepted but won't implement |
The Area dropdown's options are the strings issue-area.yml matches, so a change to one has to land in the three templates and that workflow together; an answer the workflow does not know fails its run rather than passing quietly, which is how the drift is noticed.
The path labeler never removes a label (sync-labels: false), so anything applied by hand survives a later push. It also cannot create one, because the workflow grants only pull-requests: write. A label named in .github/labeler.yml that does not exist in the repository is therefore not skipped: the action sends the whole set in a single request, so that one name costs the pull request every label it should have had, and the job says so rather than passing quietly. Create the label first, with gh label list / gh label create.