mcp-gitlab 0.9.2__tar.gz → 0.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.gitignore +3 -0
  2. mcp_gitlab-0.9.4/AGENTS.md +184 -0
  3. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/CHANGELOG.md +18 -0
  4. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/PKG-INFO +4 -4
  5. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/README.md +2 -2
  6. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/gemini-extension.json +1 -1
  7. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/llms-full.txt +4 -4
  8. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/llms.txt +1 -1
  9. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/pyproject.toml +1 -1
  10. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/server.json +3 -3
  11. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/servers/gitlab.py +29 -28
  12. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_tools.py +112 -0
  13. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/uv.lock +12 -6
  14. mcp_gitlab-0.9.2/AGENTS.md +0 -124
  15. mcp_gitlab-0.9.2/GEMINI.md +0 -33
  16. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.env.example +0 -0
  17. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  19. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/dependabot.yml +0 -0
  20. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/workflows/lint.yml +0 -0
  21. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/workflows/publish.yml +0 -0
  22. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/workflows/release.yml +0 -0
  23. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/.github/workflows/tests.yml +0 -0
  24. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/LICENSE +0 -0
  25. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/evaluations/eval.xml +0 -0
  26. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/__init__.py +0 -0
  27. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/__main__.py +0 -0
  28. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/client.py +0 -0
  29. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/config.py +0 -0
  30. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/exceptions.py +0 -0
  31. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/py.typed +0 -0
  32. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/approval-workflow.md +0 -0
  33. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/code-review.md +0 -0
  34. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/codeowners.md +0 -0
  35. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/conventional-commits.md +0 -0
  36. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/git-workflow.md +0 -0
  37. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/gitlab-ci.md +0 -0
  38. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/mr-hygiene.md +0 -0
  39. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/approve-mr.md +0 -0
  40. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/diagnose-pipeline.md +0 -0
  41. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/prepare-release.md +0 -0
  42. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/review-mr.md +0 -0
  43. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/setup-branch-protection.md +0 -0
  44. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/resources/prompts/triage-issues.md +0 -0
  45. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/servers/__init__.py +0 -0
  46. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/servers/_helpers.py +0 -0
  47. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/servers/prompts.py +0 -0
  48. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/servers/resources.py +0 -0
  49. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/src/mcp_gitlab/utils/__init__.py +0 -0
  50. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/__init__.py +0 -0
  51. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/conftest.py +0 -0
  52. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/test_links.py +0 -0
  53. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/__init__.py +0 -0
  54. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_client.py +0 -0
  55. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_config.py +0 -0
  56. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_exceptions.py +0 -0
  57. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_prompts.py +0 -0
  58. {mcp_gitlab-0.9.2 → mcp_gitlab-0.9.4}/tests/unit/test_resources.py +0 -0
@@ -35,3 +35,6 @@ Thumbs.db
35
35
  .claude/
36
36
  .cursorrules
37
37
  .cursor/
38
+
39
+ # 1Password shell-plugin config: local tool state, not project config.
40
+ .op/
@@ -0,0 +1,184 @@
1
+ # mcp-gitlab — Agent Context
2
+
3
+ MCP server exposing 83 tools, 7 resources, and 6 prompts over the GitLab REST API v4. Covers the
4
+ full project lifecycle: code, reviews, CI/CD, releases, and issue tracking. Works against
5
+ GitLab.com and self-hosted instances.
6
+
7
+ ## Architecture
8
+
9
+ - **Entry point**: `src/mcp_gitlab/__init__.py` — click CLI, loads `.env` via python-dotenv, runs the FastMCP server
10
+ - **Client**: `src/mcp_gitlab/client.py` — async httpx client with all GitLab API methods
11
+ - **Tools**: `src/mcp_gitlab/servers/gitlab.py` — all 83 FastMCP tool registrations
12
+ - **Resources**: `src/mcp_gitlab/servers/resources.py` — 7 MCP resources (4 `resource://rules/*`, 3 `resource://guides/*`), content in `src/mcp_gitlab/resources/*.md`
13
+ - **Prompts**: `src/mcp_gitlab/servers/prompts.py` — 6 MCP prompts (multi-tool workflows)
14
+ - **Helpers**: `src/mcp_gitlab/servers/_helpers.py` — cached file loader with path-traversal guard, plus GitLab URL parsers. Most tools accept a project ID, a path, *or* a full GitLab URL for `project_id`; MR and pipeline URLs also yield the iid/id
15
+ - **Config**: `src/mcp_gitlab/config.py` — `GitLabConfig` dataclass built from env vars
16
+ - **Exceptions**: `src/mcp_gitlab/exceptions.py` — `GitLabError` base; `GitLabApiError`, `GitLabAuthError`, `GitLabNotFoundError`, `GitLabWriteDisabledError`
17
+ - **Tests**: `tests/` — `unit/test_tools.py` (143 tool-level tests via the FastMCP in-memory client), plus `test_client.py`, `test_config.py`, `test_exceptions.py`, `test_prompts.py`, `test_resources.py`, and `tests/test_links.py`. Shared fixtures (`config`, `client`, `mock_api` via respx) live in `tests/conftest.py`
18
+
19
+ ## Development
20
+
21
+ ```bash
22
+ uv sync --all-extras
23
+ uv run pytest --cov
24
+ uv run ruff check .
25
+ uv run ruff format --check . # CI runs --check; formatting drift fails the build
26
+ ```
27
+
28
+ `pytest` uses `asyncio_mode = "auto"` — async tests need no marker. Ruff line length is 100,
29
+ target py310; lint rules include `S` (bandit), `EM`, `N`, `UP`. `tests/**` waives `S101`,
30
+ `S105`, `S106`.
31
+
32
+ ## Patterns
33
+
34
+ - All tools are `async def` returning JSON strings
35
+ - `_ok(data)` for success, `_err(e)` for failure; `_paginated(items)` for list responses
36
+ - Pipeline and job payloads are trimmed by `_slim_pipeline` / `_slim_job`; tools expose `slim=True` by default
37
+ - Write access control: `_check_write(ctx)` raises `GitLabWriteDisabledError` when `GITLAB_READ_ONLY=true`
38
+ - Tags: every tool tagged with `{"gitlab", "<category>", "read"|"write"}`
39
+ - Parameters use `Annotated[type, Field(description=...)]`
40
+ - Client uses httpx with `PRIVATE-TOKEN` header auth
41
+ - Project/group IDs can be numeric or URL-encoded paths
42
+ - `gitlab_list_variables` / `gitlab_list_group_variables` return `***MASKED***` for variables GitLab marks as masked
43
+
44
+ ## MCP Compliance Rules
45
+
46
+ ### Tool annotations (mandatory)
47
+ Every tool MUST have `annotations={}` with at minimum `readOnlyHint`.
48
+ - Read tools: `annotations={"readOnlyHint": True, "idempotentHint": True}`
49
+ - Non-destructive writes: `annotations={"readOnlyHint": False}`
50
+ - Destructive writes: `annotations={"destructiveHint": True, "readOnlyHint": False}`
51
+ - Idempotent writes (PUT/update): add `idempotentHint: True`
52
+
53
+ ### Tool descriptions
54
+ 1-2 sentences. Front-load what it does AND what it returns.
55
+ - Bad: "This tool gets a merge request."
56
+ - Good: "Get merge request details. Returns title, state, branches, author, diff_refs."
57
+
58
+ ### Error handling
59
+ - Every tool MUST wrap in try/except and return `_err(e)` — never raise.
60
+ - Error text MUST be actionable: what went wrong plus a suggested fix.
61
+ - Never return concatenated JSON strings — always a single valid JSON object.
62
+ - Never expose stack traces, tokens, or internal paths.
63
+
64
+ ### Parameter design
65
+ - `Annotated[type, Field(description="...")]` on every parameter.
66
+ - `Literal[...]` for known value sets instead of plain `str`.
67
+ - Every optional parameter has a default.
68
+ - Flatten — no nested dicts unless truly necessary.
69
+
70
+ ### Read-only mode
71
+ Every write tool MUST call `_check_write(ctx)` before any mutation.
72
+
73
+ ### Naming convention
74
+ - Pattern: `gitlab_{verb}_{resource}` (snake_case)
75
+ - Verbs: create, get, list, search, update, delete, merge, rebase, retry, play, cancel, award,
76
+ remove, share, unshare, compare, add, reply, resolve, approve, unapprove, subscribe, unsubscribe
77
+
78
+ ## Tool Categories (83)
79
+
80
+ | Category | Count | Operations |
81
+ |---|---|---|
82
+ | Projects | 4 | get, create, delete, update merge settings |
83
+ | Approvals | 10 | project-level approval settings; project and MR approval rules (list, create, update, delete) |
84
+ | Groups | 6 | list, get groups; share/unshare project with group; share/unshare group with group |
85
+ | Branches | 3 | list, create, delete |
86
+ | Commits | 4 | list, get, create, compare refs |
87
+ | Merge Requests | 15 | list, get, create, update, merge, merge sequence, rebase, changes/diffs, approve, unapprove, get approvals, list pipelines, list commits, subscribe, unsubscribe |
88
+ | MR Notes | 6 | list, add, update, delete notes; award/remove emoji |
89
+ | MR Discussions | 4 | list, create, reply, resolve |
90
+ | Pipelines | 5 | list, get, create, retry, cancel |
91
+ | Jobs | 4 | retry, play, cancel, get job log |
92
+ | Tags | 4 | list, get, create, delete |
93
+ | Releases | 5 | list, get, create, update, delete |
94
+ | CI/CD Variables | 8 | project and group variables (list, create, update, delete) |
95
+ | Issues | 5 | list, get, create, update, add comment |
96
+
97
+ Share/unshare tools are counted under Groups, not Projects. There is no `gitlab_list_jobs` —
98
+ get job IDs from `gitlab_get_pipeline(..., include_jobs=True)`.
99
+
100
+ ## Common Workflows
101
+
102
+ - **Code review**: `gitlab_list_mrs` → `gitlab_mr_changes` → `gitlab_list_mr_discussions` → `gitlab_add_mr_note` or `gitlab_create_mr_discussion` → `gitlab_resolve_discussion`
103
+ - **Pipeline debugging**: `gitlab_list_pipelines` → `gitlab_get_pipeline` (`include_jobs=True`) → `gitlab_get_job_log` → `gitlab_retry_job`
104
+ - **Release**: `gitlab_list_commits` → `gitlab_compare` → `gitlab_create_tag` → `gitlab_create_release`
105
+ - **Branch protection**: `gitlab_list_project_approval_rules` → `gitlab_create_project_approval_rule` → `gitlab_update_project_merge_settings`
106
+ - **Issue triage**: `gitlab_list_issues` → `gitlab_get_issue` → `gitlab_update_issue` → `gitlab_add_issue_comment`
107
+
108
+ ## Prompts
109
+
110
+ Prompt content lives as `.md` files in `src/mcp_gitlab/resources/prompts/`, loaded by
111
+ `servers/prompts.py` via `_load_prompt()` (`string.Template.safe_substitute` for parameters) and
112
+ registered with `@mcp.prompt()`. Each returns `list[Message]`: a user message (workflow template)
113
+ plus an assistant acknowledgment.
114
+
115
+ | Prompt | Purpose | Tags |
116
+ |---|---|---|
117
+ | `review_mr` | MR review workflow | gitlab, review |
118
+ | `approve_mr` | MR approval workflow | gitlab, review, approvals |
119
+ | `diagnose_pipeline` | CI debug workflow | gitlab, ci |
120
+ | `prepare_release` | Release preparation | gitlab, release |
121
+ | `setup_branch_protection` | Branch protection setup | gitlab, settings |
122
+ | `triage_issues` | Issue triage workflow | gitlab, issues |
123
+
124
+ ## Environment Variables
125
+
126
+ | Variable | Required | Default | Notes |
127
+ |---|---|---|---|
128
+ | `GITLAB_URL` | yes | — | Instance base URL; trailing slash stripped, `/api/v4` appended |
129
+ | token (see below) | yes | — | Personal access token, OAuth2 token, or `$CI_JOB_TOKEN` |
130
+ | `GITLAB_READ_ONLY` | no | `false` | `true`/`1`/`yes` disables all writes and deletes, enforced before any API call |
131
+ | `GITLAB_TIMEOUT` | no | `30` | Request timeout in seconds |
132
+ | `GITLAB_SSL_VERIFY` | no | `true` | `false`/`0`/`no` skips verification — self-signed certs only |
133
+
134
+ Token is read from the first of `GITLAB_TOKEN`, `GITLAB_PAT`, `GITLAB_PERSONAL_ACCESS_TOKEN`,
135
+ `GITLAB_API_TOKEN` that is set. Scope `api` for full access, `read_api` for read-only deployments.
136
+ Tokens are never persisted — they are read from the environment at startup.
137
+
138
+ CLI flags override env: `--gitlab-url`, `--gitlab-token`, `--read-only`, plus
139
+ `--transport {stdio,sse,streamable-http}` with `--host` (default `127.0.0.1`) and `--port`
140
+ (default `8000`); host/port apply only to non-stdio transports.
141
+
142
+ ## Release Workflow
143
+
144
+ Releases run through GitHub Actions — never bump versions manually.
145
+
146
+ ```bash
147
+ gh workflow run release.yml -f bump=minor # 0.9.0 → 0.10.0
148
+ gh workflow run release.yml -f bump=patch # 0.9.0 → 0.9.1
149
+ gh workflow run release.yml -f bump=major # 0.9.0 → 1.0.0
150
+ gh workflow run release.yml -f bump=minor -f dry_run=true # preview changelog, no push
151
+ ```
152
+
153
+ 1. `release.yml` (workflow_dispatch) — bumps the version in `pyproject.toml`, `llms.txt`,
154
+ `llms-full.txt`, `server.json`, `gemini-extension.json`; regenerates `uv.lock`; prepends a
155
+ CHANGELOG.md entry; creates the release commit and tag via the GitHub API.
156
+ 2. `publish.yml` (triggered by a `v*` tag push) — builds the wheel, publishes to PyPI, creates the
157
+ GitHub Release with auto-generated notes, then publishes to the MCP Registry.
158
+
159
+ Rules:
160
+ - Never edit the `pyproject.toml` version directly — the workflow owns it.
161
+ - Never create tags manually — the workflow creates them.
162
+ - Commit messages must follow Conventional Commits (`feat:`, `fix:`, `docs:`, …) for changelog generation.
163
+ - The release commit is authored by `github-actions[bot]` with message `chore(release): X.Y.Z`.
164
+
165
+ ## Documentation Freshness (mandatory)
166
+
167
+ When a changeset adds, removes, or modifies tools, resources, or prompts, update ALL of these in
168
+ the same commit:
169
+
170
+ - `README.md` — counts in heading and intro, tool table, full tool reference, usage examples, permissions table
171
+ - `llms.txt` — count in tagline and documentation link
172
+ - `llms-full.txt` — count in tagline, documentation link, full tool reference section
173
+ - `AGENTS.md` — counts in intro, tool category table
174
+ - `GEMINI.md` — count in intro, tool categories, common workflows
175
+ - `server.json` — description field (≤100 chars)
176
+
177
+ Checklist: counts match the actual registered tools/resources/prompts; the category list is
178
+ complete; new entries appear in the right sections with parameters and annotations.
179
+
180
+ ## Known Limitations
181
+
182
+ - 83 tools in one server file, well past the 5-15 guideline. Split by category if it is refactored.
183
+ - Errors come back as successful tool results carrying `{"error": ...}` (soft-error pattern);
184
+ callers must inspect the JSON content rather than relying on protocol-level errors.
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.4] - 2026-09-20
4
+
5
+ ### Bug Fixes
6
+ - fix: correct error masking in merge sequence, tighten docs and messages (7b43094)
7
+
8
+ ### Documentation
9
+ - docs: consolidate GEMINI.md into AGENTS.md (bcfb75f)
10
+
11
+ ### Chores
12
+ - chore: ignore local 1Password plugin state (8d0b554)
13
+
14
+
15
+ ## [0.9.3] - 2026-08-22
16
+
17
+ ### Chores
18
+ - chore(deps): bump mcp in the uv group across 1 directory (fcf3ec8)
19
+
20
+
3
21
  ## [0.9.2] - 2026-07-08
4
22
 
5
23
  ### Chores
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mcp-gitlab
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: MCP server for GitLab API — projects, MRs, pipelines, CI/CD variables, approvals, and more
5
5
  Project-URL: Homepage, https://github.com/vish288/mcp-gitlab
6
6
  Project-URL: Repository, https://github.com/vish288/mcp-gitlab
@@ -156,7 +156,7 @@ These accept any of the following token types:
156
156
  | **Groups** | 6 | list, get, share/unshare project, share/unshare group |
157
157
  | **Branches** | 3 | list, create, delete |
158
158
  | **Commits** | 4 | list, get (with diff), create, compare |
159
- | **Merge Requests** | 16 | list, get, create, update, merge, merge-sequence, rebase, changes, approve, unapprove, get approvals, list reviewers, list pipelines, list commits, subscribe, unsubscribe |
159
+ | **Merge Requests** | 15 | list, get, create, update, merge, merge-sequence, rebase, changes, approve, unapprove, get approvals, list pipelines, list commits, subscribe, unsubscribe |
160
160
  | **MR Notes** | 6 | list, add, delete, update, award emoji, remove emoji |
161
161
  | **MR Discussions** | 4 | list, create (inline + multi-line), reply, resolve |
162
162
  | **Pipelines** | 5 | list, get (with jobs), create, retry, cancel |
@@ -268,7 +268,7 @@ These accept any of the following token types:
268
268
  | `gitlab_retry_job` | Retry a job |
269
269
  | `gitlab_play_job` | Trigger manual job |
270
270
  | `gitlab_cancel_job` | Cancel a job |
271
- | `gitlab_get_job_log` | Get job log output |
271
+ | `gitlab_get_job_log` | Get job log output (last 200 lines by default; `tail_lines=0` for all) |
272
272
 
273
273
  ### Tags
274
274
  | Tool | Description |
@@ -120,7 +120,7 @@ These accept any of the following token types:
120
120
  | **Groups** | 6 | list, get, share/unshare project, share/unshare group |
121
121
  | **Branches** | 3 | list, create, delete |
122
122
  | **Commits** | 4 | list, get (with diff), create, compare |
123
- | **Merge Requests** | 16 | list, get, create, update, merge, merge-sequence, rebase, changes, approve, unapprove, get approvals, list reviewers, list pipelines, list commits, subscribe, unsubscribe |
123
+ | **Merge Requests** | 15 | list, get, create, update, merge, merge-sequence, rebase, changes, approve, unapprove, get approvals, list pipelines, list commits, subscribe, unsubscribe |
124
124
  | **MR Notes** | 6 | list, add, delete, update, award emoji, remove emoji |
125
125
  | **MR Discussions** | 4 | list, create (inline + multi-line), reply, resolve |
126
126
  | **Pipelines** | 5 | list, get (with jobs), create, retry, cancel |
@@ -232,7 +232,7 @@ These accept any of the following token types:
232
232
  | `gitlab_retry_job` | Retry a job |
233
233
  | `gitlab_play_job` | Trigger manual job |
234
234
  | `gitlab_cancel_job` | Cancel a job |
235
- | `gitlab_get_job_log` | Get job log output |
235
+ | `gitlab_get_job_log` | Get job log output (last 200 lines by default; `tail_lines=0` for all) |
236
236
 
237
237
  ### Tags
238
238
  | Tool | Description |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-gitlab",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "MCP server for GitLab API \u2014 projects, MRs, pipelines, CI/CD variables, approvals, and more",
5
5
  "mcpServers": {
6
6
  "mcp-gitlab": {
@@ -5,7 +5,7 @@
5
5
  MCP server that wraps the GitLab REST API. Works with GitLab.com and self-hosted instances (CE/EE). No GitLab Duo or Premium required. Built with FastMCP, httpx, and Pydantic.
6
6
 
7
7
  - **Install**: `uvx mcp-gitlab`
8
- - **Version**: 0.9.2
8
+ - **Version**: 0.9.4
9
9
  - **Python**: >=3.10
10
10
  - **License**: MIT
11
11
  - **Transport**: stdio (default), SSE, streamable-http
@@ -133,7 +133,7 @@ uvx mcp-gitlab --gitlab-url https://gitlab.example.com --gitlab-token glpat-xxx
133
133
  | `gitlab_create_commit` | Create a commit with multiple file actions |
134
134
  | `gitlab_compare` | Compare two branches, tags, or commits |
135
135
 
136
- ### Merge Requests (16)
136
+ ### Merge Requests (15)
137
137
 
138
138
  | Tool | Description |
139
139
  |------|-------------|
@@ -190,7 +190,7 @@ uvx mcp-gitlab --gitlab-url https://gitlab.example.com --gitlab-token glpat-xxx
190
190
  | `gitlab_retry_job` | Retry a failed job |
191
191
  | `gitlab_play_job` | Trigger a manual job |
192
192
  | `gitlab_cancel_job` | Cancel a running job |
193
- | `gitlab_get_job_log` | Get the log (trace) output of a job |
193
+ | `gitlab_get_job_log` | Get the log (trace) output of a job — last 200 lines by default, `tail_lines=0` for the whole log |
194
194
 
195
195
  ### Tags (4)
196
196
 
@@ -234,7 +234,7 @@ uvx mcp-gitlab --gitlab-url https://gitlab.example.com --gitlab-token glpat-xxx
234
234
  | `gitlab_update_issue` | Update an existing issue |
235
235
  | `gitlab_add_issue_comment` | Add a comment to an issue |
236
236
 
237
- ## Resources (6)
237
+ ## Resources (7)
238
238
 
239
239
  The server exposes curated workflow guides as MCP resources that clients can read on demand.
240
240
 
@@ -5,7 +5,7 @@
5
5
  MCP server that wraps the GitLab REST API. Works with GitLab.com and self-hosted instances (CE/EE). No GitLab Duo or Premium required. Built with FastMCP, httpx, and Pydantic.
6
6
 
7
7
  - **Install**: `uvx mcp-gitlab`
8
- - **Version**: 0.9.2
8
+ - **Version**: 0.9.4
9
9
  - **Python**: >=3.10
10
10
  - **License**: MIT
11
11
  - **Transport**: stdio (default), SSE, streamable-http
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-gitlab"
3
- version = "0.9.2"
3
+ version = "0.9.4"
4
4
  description = "MCP server for GitLab API — projects, MRs, pipelines, CI/CD variables, approvals, and more"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -6,19 +6,19 @@
6
6
  "url": "https://github.com/vish288/mcp-gitlab",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.9.2",
9
+ "version": "0.9.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "pypi",
13
13
  "identifier": "mcp-gitlab",
14
- "version": "0.9.2",
14
+ "version": "0.9.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
18
18
  "environmentVariables": [
19
19
  {
20
20
  "name": "GITLAB_TOKEN",
21
- "description": "GitLab personal access token (also accepts GITLAB_PAT or GITLAB_API_TOKEN)",
21
+ "description": "GitLab personal access token (also accepts GITLAB_PAT, GITLAB_PERSONAL_ACCESS_TOKEN, or GITLAB_API_TOKEN)",
22
22
  "isRequired": true,
23
23
  "format": "string",
24
24
  "isSecret": true
@@ -726,7 +726,9 @@ async def gitlab_share_group_with_group(
726
726
  _check_write(ctx)
727
727
  level = ACCESS_LEVELS.get(access_level.lower())
728
728
  if level is None:
729
- return _ok({"error": f"Invalid access level: {access_level}"})
729
+ return _ok(
730
+ {"error": f"Invalid access level: {access_level}. Use: {', '.join(ACCESS_LEVELS)}"}
731
+ )
730
732
  await _get_client(ctx).share_group_with_group(target_group_id, source_group_id, level)
731
733
  return _ok({"status": "shared"})
732
734
  except Exception as e:
@@ -1031,10 +1033,7 @@ async def gitlab_get_mr(
1031
1033
  ) -> str:
1032
1034
  """Get merge request details.
1033
1035
 
1034
-
1035
-
1036
1036
  Returns title, state, source/target branches, author, diff_refs, and merge status.
1037
-
1038
1037
  """
1039
1038
  try:
1040
1039
  data = await _get_client(ctx).get_merge_request(project_id, mr_iid)
@@ -1111,7 +1110,7 @@ async def gitlab_update_mr(
1111
1110
  draft: Annotated[bool | None, Field(description="Set draft status")] = None,
1112
1111
  state_event: Annotated[str | None, Field(description="close or reopen")] = None,
1113
1112
  ) -> str:
1114
- """Update an MR's title, description, labels, assignees, milestone, or state event.
1113
+ """Update an MR's title, description, target branch, labels, squash/draft flags, or state.
1115
1114
 
1116
1115
  Returns the updated MR object.
1117
1116
  """
@@ -1210,10 +1209,10 @@ async def gitlab_merge_mr_sequence(
1210
1209
 
1211
1210
  Returns a per-MR result list with merged/failed status and any error.
1212
1211
  """
1212
+ merged: list[int] = []
1213
1213
  try:
1214
1214
  _check_write(ctx)
1215
1215
  client = _get_client(ctx)
1216
- merged: list[int] = []
1217
1216
  params: dict[str, Any] = {}
1218
1217
  if squash is not None:
1219
1218
  params["squash"] = squash
@@ -1238,13 +1237,9 @@ async def gitlab_merge_mr_sequence(
1238
1237
 
1239
1238
  return _ok({"status": "all_merged", "merged": merged})
1240
1239
  except Exception as e:
1241
- detail: dict[str, Any] = {"error": str(e), "merged_so_far": merged}
1242
- from ..exceptions import GitLabApiError
1243
-
1244
- if isinstance(e, GitLabApiError):
1245
- detail["status_code"] = e.status_code
1246
- detail["body"] = e.body
1247
- return json.dumps(detail, indent=2, ensure_ascii=False)
1240
+ detail = json.loads(_err(e))
1241
+ detail["merged_so_far"] = merged
1242
+ return _ok(detail)
1248
1243
 
1249
1244
 
1250
1245
  @mcp.tool(
@@ -1455,10 +1450,7 @@ async def gitlab_list_mr_discussions(
1455
1450
  ) -> str:
1456
1451
  """List discussions on a merge request.
1457
1452
 
1458
-
1459
-
1460
- Returns discussion threads with notes, excluding system notes.
1461
-
1453
+ Returns discussion threads with notes, excluding system-only threads.
1462
1454
  """
1463
1455
  try:
1464
1456
  data = await _get_client(ctx).list_mr_discussions(project_id, mr_iid)
@@ -1818,11 +1810,8 @@ async def gitlab_get_pipeline(
1818
1810
  ) -> str:
1819
1811
  """Get pipeline details, optionally with jobs.
1820
1812
 
1821
-
1822
-
1823
1813
  Returns id, status, ref, timing, web_url.
1824
1814
  Jobs include id, name, stage, status, timing, failure_reason, web_url.
1825
-
1826
1815
  """
1827
1816
  try:
1828
1817
  client = _get_client(ctx)
@@ -1989,9 +1978,15 @@ async def gitlab_get_job_log(
1989
1978
  str, Field(description="Project ID, path, or full GitLab URL", min_length=1)
1990
1979
  ],
1991
1980
  job_id: Annotated[int, Field(description="Job ID")],
1992
- tail_lines: Annotated[int, Field(description="Number of lines from the end to return")] = 200,
1981
+ tail_lines: Annotated[
1982
+ int,
1983
+ Field(description="Lines to return from the end of the log; 0 returns the whole log", ge=0),
1984
+ ] = 200,
1993
1985
  ) -> str:
1994
- """Get a job's full log trace as text."""
1986
+ """Get a job's log trace, truncated to the last tail_lines lines (default 200).
1987
+
1988
+ Returns {log, total_lines, shown_lines}. Pass tail_lines=0 for the whole log.
1989
+ """
1995
1990
  try:
1996
1991
  log_text = await _get_client(ctx).get_job_log(project_id, job_id)
1997
1992
  lines = log_text.splitlines()
@@ -2107,7 +2102,7 @@ async def gitlab_delete_tag(
2107
2102
  ],
2108
2103
  tag_name: Annotated[str, Field(description="Tag name to delete", min_length=1)],
2109
2104
  ) -> str:
2110
- """Delete a tag. Returns a {status: deleted, tag_name} confirmation."""
2105
+ """Delete a tag. Returns a {status: deleted, tag} confirmation."""
2111
2106
  try:
2112
2107
  _check_write(ctx)
2113
2108
  await _get_client(ctx).delete_tag(project_id, tag_name)
@@ -2226,7 +2221,7 @@ async def gitlab_update_release(
2226
2221
  description: Annotated[str | None, Field(description="New release description")] = None,
2227
2222
  released_at: Annotated[str | None, Field(description="New release date (ISO 8601)")] = None,
2228
2223
  ) -> str:
2229
- """Update a release's name, description, or milestones. Returns the updated release object."""
2224
+ """Update a release's name, description, or release date. Returns the updated release."""
2230
2225
  try:
2231
2226
  _check_write(ctx)
2232
2227
  params: dict[str, Any] = {}
@@ -2255,7 +2250,7 @@ async def gitlab_delete_release(
2255
2250
  ) -> str:
2256
2251
  """Delete a release (the underlying tag is preserved).
2257
2252
 
2258
- Returns the deleted release's metadata.
2253
+ Returns a {status: deleted, tag_name} confirmation.
2259
2254
  """
2260
2255
  try:
2261
2256
  _check_write(ctx)
@@ -2358,10 +2353,16 @@ async def gitlab_update_variable(
2358
2353
  protected: Annotated[bool | None, Field(description="Protected branches only")] = None,
2359
2354
  masked: Annotated[bool | None, Field(description="Mask in logs")] = None,
2360
2355
  raw: Annotated[bool | None, Field(description="Do not expand references")] = None,
2361
- environment_scope: Annotated[str | None, Field(description="Environment scope")] = None,
2356
+ environment_scope: Annotated[
2357
+ str | None,
2358
+ Field(description="Environment scope filter — selects which scoped variable to update"),
2359
+ ] = None,
2362
2360
  description: Annotated[str | None, Field(description="Variable description")] = None,
2363
2361
  ) -> str:
2364
- """Update a project CI/CD variable. Returns the updated variable object."""
2362
+ """Update a project CI/CD variable. Returns the updated variable object.
2363
+
2364
+ environment_scope selects which scoped variable to update; it does not change the scope.
2365
+ """
2365
2366
  try:
2366
2367
  _check_write(ctx)
2367
2368
  params: dict[str, Any] = {"value": value}
@@ -2652,7 +2653,7 @@ async def gitlab_update_issue(
2652
2653
  state_event: Annotated[str | None, Field(description="close or reopen")] = None,
2653
2654
  weight: Annotated[int | None, Field(description="Issue weight")] = None,
2654
2655
  ) -> str:
2655
- """Update an issue's title, description, labels, assignees, milestone, or state event.
2656
+ """Update an issue's title, description, labels, assignees, weight, or state event.
2656
2657
 
2657
2658
  Returns the updated issue object.
2658
2659
  """
@@ -2329,3 +2329,115 @@ class TestOptionalParams:
2329
2329
  )
2330
2330
  parsed = _parse(result)
2331
2331
  assert parsed["title"] == "Updated"
2332
+
2333
+
2334
+ # ═══════════════════════════════════════════════════════
2335
+ # Regression tests — defects found by audit
2336
+ # ═══════════════════════════════════════════════════════
2337
+
2338
+
2339
+ class TestDocstringContracts:
2340
+ """Tools whose docstrings promise a specific return shape must deliver it."""
2341
+
2342
+ async def test_merge_mr_sequence_readonly_blocked(self, readonly_client):
2343
+ """Read-only mode must return a JSON error, not blow up on an unbound local."""
2344
+ client, router = readonly_client
2345
+ result = await client.call_tool(
2346
+ "gitlab_merge_mr_sequence",
2347
+ {"project_id": "123", "mr_iids": [1, 2]},
2348
+ )
2349
+ parsed = _parse(result)
2350
+ assert "error" in parsed
2351
+ assert "read-only" in parsed["hint"].lower()
2352
+ assert parsed["merged_so_far"] == []
2353
+
2354
+ async def test_merge_mr_sequence_partial_failure_reports_progress(self, tool_client):
2355
+ """A mid-sequence API failure still reports which MRs already merged, plus a hint."""
2356
+ client, router = tool_client
2357
+ router.get("/projects/123/merge_requests/1").mock(
2358
+ return_value=Response(200, json={"iid": 1, "detailed_merge_status": "mergeable"})
2359
+ )
2360
+ router.put("/projects/123/merge_requests/1/merge").mock(
2361
+ return_value=Response(200, json={"iid": 1, "state": "merged"})
2362
+ )
2363
+ router.get("/projects/123/merge_requests/2").mock(
2364
+ return_value=Response(404, json={"message": "404 Not found"})
2365
+ )
2366
+ result = await client.call_tool(
2367
+ "gitlab_merge_mr_sequence",
2368
+ {"project_id": "123", "mr_iids": [1, 2]},
2369
+ )
2370
+ parsed = _parse(result)
2371
+ assert parsed["merged_so_far"] == [1]
2372
+ assert parsed["status_code"] == 404
2373
+ assert "hint" in parsed
2374
+
2375
+ async def test_delete_tag_returns_tag_key(self, tool_client):
2376
+ """Docstring promises {status: deleted, tag} — not tag_name."""
2377
+ client, router = tool_client
2378
+ router.delete("/projects/123/repository/tags/v1.0").mock(return_value=Response(204))
2379
+ result = await client.call_tool(
2380
+ "gitlab_delete_tag", {"project_id": "123", "tag_name": "v1.0"}
2381
+ )
2382
+ parsed = _parse(result)
2383
+ assert parsed == {"status": "deleted", "tag": "v1.0"}
2384
+
2385
+ async def test_delete_release_returns_confirmation_not_metadata(self, tool_client):
2386
+ """Docstring promises {status: deleted, tag_name}, not the release object."""
2387
+ client, router = tool_client
2388
+ router.delete("/projects/123/releases/v1.0").mock(return_value=Response(204))
2389
+ result = await client.call_tool(
2390
+ "gitlab_delete_release", {"project_id": "123", "tag_name": "v1.0"}
2391
+ )
2392
+ parsed = _parse(result)
2393
+ assert parsed == {"status": "deleted", "tag_name": "v1.0"}
2394
+
2395
+ async def test_get_job_log_zero_tail_returns_whole_log(self, tool_client):
2396
+ """Docstring promises tail_lines=0 returns the whole log."""
2397
+ client, router = tool_client
2398
+ lines = "\n".join(f"line {i}" for i in range(300))
2399
+ router.get("/projects/123/jobs/1/trace").mock(return_value=Response(200, text=lines))
2400
+ result = await client.call_tool(
2401
+ "gitlab_get_job_log",
2402
+ {"project_id": "123", "job_id": 1, "tail_lines": 0},
2403
+ )
2404
+ parsed = _parse(result)
2405
+ assert parsed["shown_lines"] == 300
2406
+ assert parsed["total_lines"] == 300
2407
+
2408
+ async def test_get_job_log_rejects_negative_tail(self, tool_client):
2409
+ """Negative tail_lines used to silently drop lines off the front; now rejected."""
2410
+ client, router = tool_client
2411
+ with pytest.raises(Exception, match="tail_lines"):
2412
+ await client.call_tool(
2413
+ "gitlab_get_job_log",
2414
+ {"project_id": "123", "job_id": 1, "tail_lines": -5},
2415
+ )
2416
+
2417
+ async def test_share_group_with_group_invalid_level_lists_valid_levels(self, tool_client):
2418
+ """Error message must list the valid levels, matching its project-share sibling."""
2419
+ client, router = tool_client
2420
+ result = await client.call_tool(
2421
+ "gitlab_share_group_with_group",
2422
+ {"target_group_id": "9", "source_group_id": 1, "access_level": "bogus"},
2423
+ )
2424
+ parsed = _parse(result)
2425
+ assert "bogus" in parsed["error"]
2426
+ assert "maintainer" in parsed["error"]
2427
+
2428
+ async def test_tool_descriptions_have_no_blank_line_runs(self, tool_client):
2429
+ """Docstrings ship verbatim as MCP tool descriptions — no stray blank-line runs."""
2430
+ client, router = tool_client
2431
+ tools = await client.list_tools()
2432
+ offenders = [t.name for t in tools if t.description and "\n\n\n" in t.description]
2433
+ assert offenders == []
2434
+
2435
+ async def test_update_docstrings_only_name_real_params(self, tool_client):
2436
+ """Docstrings must not advertise parameters the tool does not accept."""
2437
+ client, router = tool_client
2438
+ tools = {t.name: t for t in await client.list_tools()}
2439
+ for name in ("gitlab_update_mr", "gitlab_update_issue", "gitlab_update_release"):
2440
+ desc = tools[name].description.lower()
2441
+ params = tools[name].inputSchema["properties"]
2442
+ assert "milestone" not in desc or any("milestone" in p for p in params)
2443
+ assert "assignee" not in desc or any("assignee" in p for p in params)
@@ -1,6 +1,12 @@
1
1
  version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.10"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
6
+ "python_full_version >= '3.14' and sys_platform != 'win32'",
7
+ "python_full_version < '3.14' and sys_platform == 'win32'",
8
+ "python_full_version < '3.14' and sys_platform != 'win32'",
9
+ ]
4
10
 
5
11
  [[package]]
6
12
  name = "aiofile"
@@ -802,7 +808,7 @@ wheels = [
802
808
 
803
809
  [[package]]
804
810
  name = "mcp"
805
- version = "1.26.0"
811
+ version = "1.28.1"
806
812
  source = { registry = "https://pypi.org/simple" }
807
813
  dependencies = [
808
814
  { name = "anyio" },
@@ -820,14 +826,14 @@ dependencies = [
820
826
  { name = "typing-inspection" },
821
827
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
822
828
  ]
823
- sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
829
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
824
830
  wheels = [
825
- { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
831
+ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
826
832
  ]
827
833
 
828
834
  [[package]]
829
835
  name = "mcp-gitlab"
830
- version = "0.9.2"
836
+ version = "0.9.4"
831
837
  source = { editable = "." }
832
838
  dependencies = [
833
839
  { name = "click" },
@@ -1593,8 +1599,8 @@ name = "secretstorage"
1593
1599
  version = "3.5.0"
1594
1600
  source = { registry = "https://pypi.org/simple" }
1595
1601
  dependencies = [
1596
- { name = "cryptography" },
1597
- { name = "jeepney" },
1602
+ { name = "cryptography", marker = "sys_platform != 'win32'" },
1603
+ { name = "jeepney", marker = "sys_platform != 'win32'" },
1598
1604
  ]
1599
1605
  sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
1600
1606
  wheels = [
@@ -1,124 +0,0 @@
1
- # mcp-gitlab — Agent Context
2
-
3
- MCP server providing 83 tools for the GitLab REST API v4.
4
-
5
- ## Architecture
6
-
7
- - **Entry point**: `src/mcp_gitlab/__init__.py` — click CLI, loads env, runs FastMCP server
8
- - **Client**: `src/mcp_gitlab/client.py` — async httpx client with all GitLab API methods
9
- - **Tools**: `src/mcp_gitlab/servers/gitlab.py` — all FastMCP tool registrations
10
- - **Resources**: `src/mcp_gitlab/servers/resources.py` — 6 MCP resources (workflow guides)
11
- - **Prompts**: `src/mcp_gitlab/servers/prompts.py` — 5 MCP prompts (multi-tool workflows)
12
- - **Config**: `src/mcp_gitlab/config.py` — `GitLabConfig` dataclass from env vars
13
- - **Exceptions**: `src/mcp_gitlab/exceptions.py` — `GitLabApiError`, `GitLabAuthError`, etc.
14
- - **Tests**: `tests/unit/test_tools.py` — 120+ tool-level tests via FastMCP test client
15
-
16
- ## Patterns
17
-
18
- - All tools are `async def` returning JSON strings
19
- - Error handling: try/except wrapping every tool, returning `{"error": ...}` JSON
20
- - Write access control: `_check_write(ctx)` raises `GitLabWriteDisabledError` when `GITLAB_READ_ONLY=true`
21
- - Tags: every tool tagged with `{"gitlab", "<category>", "read"|"write"}`
22
- - Parameters use `Annotated[type, Field(description=...)]`
23
- - Client uses httpx with `PRIVATE-TOKEN` header auth
24
- - Project/group IDs can be numeric or URL-encoded paths
25
-
26
- ## MCP Compliance Rules
27
-
28
- ### Tool Annotations (MANDATORY)
29
- Every tool MUST have `annotations={}` with at minimum `readOnlyHint`.
30
- - Read tools: `annotations={"readOnlyHint": True, "idempotentHint": True}`
31
- - Non-destructive write tools: `annotations={"readOnlyHint": False}`
32
- - Destructive write tools: `annotations={"destructiveHint": True, "readOnlyHint": False}`
33
- - Idempotent writes (PUT/update): add `idempotentHint: True`
34
-
35
- ### Tool Descriptions
36
- - 1-2 sentences. Front-load what it does AND what it returns.
37
- - Bad: "This tool gets a merge request."
38
- - Good: "Get merge request details. Returns title, state, branches, author, diff_refs."
39
-
40
- ### Error Handling
41
- - Every tool MUST wrap in try/except and return `_err(e)` — never raise.
42
- - Error text MUST be actionable: include what went wrong and suggest a fix.
43
- - Never return concatenated JSON strings — always a single valid JSON object.
44
- - Never expose stack traces, tokens, or internal paths.
45
-
46
- ### Parameter Design
47
- - Use `Annotated[type, Field(description="...")]` on every parameter.
48
- - Use `Literal[...]` for known value sets instead of plain `str`.
49
- - Every optional parameter must have a default.
50
- - Flatten — no nested dicts unless truly necessary.
51
-
52
- ### Read-Only Mode
53
- - Every write tool MUST call `_check_write(ctx)` before any mutation.
54
-
55
- ### Naming Convention
56
- - Pattern: `gitlab_{verb}_{resource}` (snake_case)
57
- - Verbs: create, get, list, search, update, delete, merge, rebase, retry, play, cancel, award, remove, share, unshare, compare, add, reply, resolve, approve, unapprove, subscribe, unsubscribe
58
-
59
- ## Tool Categories
60
-
61
- Projects (4), Approvals (10), Groups (6), Branches (3), Commits (4), Merge Requests (15), MR Notes (6), MR Discussions (4), Pipelines (5), Jobs (4), Tags (4), Releases (5), CI/CD Variables (8), Issues (5)
62
-
63
- ## Environment Variables
64
-
65
- - `GITLAB_URL` (required) — GitLab instance base URL
66
- - `GITLAB_TOKEN` or `GITLAB_PAT` (required) — Personal access token
67
- - `GITLAB_READ_ONLY` — disable mutations
68
- - `GITLAB_TIMEOUT` — request timeout seconds
69
- - `GITLAB_SSL_VERIFY` — SSL verification toggle
70
-
71
- ## Release Workflow
72
-
73
- Releases are handled via GitHub Actions — never bump versions manually.
74
-
75
- ### How to release
76
-
77
- ```bash
78
- # From the repo directory:
79
- gh workflow run release.yml -f bump=minor # 0.4.0 → 0.5.0
80
- gh workflow run release.yml -f bump=patch # 0.5.0 → 0.5.1
81
- gh workflow run release.yml -f bump=major # 0.5.0 → 1.0.0
82
-
83
- # Dry run (preview changelog, no push):
84
- gh workflow run release.yml -f bump=minor -f dry_run=true
85
- ```
86
-
87
- ### What happens
88
-
89
- 1. `release.yml` (workflow_dispatch) — bumps the version in `pyproject.toml`, `llms.txt`, `llms-full.txt`, `server.json`, `gemini-extension.json`; regenerates `uv.lock`; prepends a CHANGELOG.md entry; creates the release commit + tag via the GitHub API
90
- 2. `publish.yml` (triggered by `v*` tag push) — builds wheel, publishes to PyPI, creates GitHub Release with auto-generated notes, then publishes to the MCP Registry
91
-
92
- ### Rules
93
- - Never edit `pyproject.toml` version directly — the workflow owns it
94
- - Never create tags manually — the workflow creates them
95
- - Commit messages must follow conventional commits (`feat:`, `fix:`, `docs:`, etc.) for changelog generation
96
- - The release commit is authored by `github-actions[bot]` with message `chore(release): X.Y.Z`
97
-
98
- ## Prompts
99
-
100
- Prompts follow the resources pattern: prompt content lives as `.md` files in `src/mcp_gitlab/resources/prompts/`, loaded by `servers/prompts.py` via `_load_prompt()`, registered with `@mcp.prompt()`. Each prompt returns `list[Message]` with a user message (workflow template) and an assistant message (acknowledgment).
101
-
102
- - `review_mr` — MR review workflow (tags: gitlab, review)
103
- - `diagnose_pipeline` — CI debug workflow (tags: gitlab, ci)
104
- - `prepare_release` — Release preparation (tags: gitlab, release)
105
- - `setup_branch_protection` — Branch protection setup (tags: gitlab, settings)
106
- - `triage_issues` — Issue triage workflow (tags: gitlab, issues)
107
-
108
- ## Documentation Freshness (MANDATORY)
109
-
110
- When any changeset adds, removes, or modifies tools, resources, or prompts, ALL documentation files MUST be updated in the same commit:
111
-
112
- - `README.md` — tool count in heading + intro, tool table, full tool reference, usage examples, permissions table
113
- - `llms.txt` — tool count in tagline and documentation link
114
- - `llms-full.txt` — tool count in tagline, documentation link, full tool reference section
115
- - `AGENTS.md` — tool count in intro, tool categories list
116
- - `GEMINI.md` — tool count in intro, tool categories, common workflows
117
- - `server.json` — description field (<=100 chars)
118
-
119
- Checklist: verify tool count matches actual registered tools, verify category list is complete, verify new tools appear in correct sections with parameters and annotations.
120
-
121
- ## Known Limitations / Future Work
122
-
123
- - 83 tools in one server file (exceeds 5-15 guideline). Consider splitting by category in a future refactor.
124
- - Errors are returned as successful tool results with `{"error": ...}` (soft-error pattern). Callers must inspect JSON content.
@@ -1,33 +0,0 @@
1
- # mcp-gitlab — Gemini CLI Extension Context
2
-
3
- MCP server providing 83 tools, 7 resources, and 6 prompts for interacting with the GitLab API. Covers the full lifecycle of GitLab projects: code, reviews, CI/CD, releases, and issue tracking.
4
-
5
- ## Tool Categories
6
-
7
- - **Projects** — get, create, delete, update merge settings, share/unshare with groups
8
- - **Merge Requests** — list, get, create, update, merge, rebase, view changes and diffs, approve, unapprove, get approvals, list pipelines, list commits, subscribe, unsubscribe
9
- - **MR Reviews** — list/add/update/delete notes, list/create discussions, reply to and resolve discussions, award/remove emoji
10
- - **MR Approvals** — project-level and MR-level approval rules (list, create, update, delete)
11
- - **Pipelines & Jobs** — list/get/create/retry/cancel pipelines, retry/play/cancel jobs, get job logs
12
- - **Branches** — list, create, delete
13
- - **Commits** — list, get, create, compare refs
14
- - **Tags & Releases** — list/get/create/delete tags, list/get/create/update/delete releases
15
- - **CI/CD Variables** — project and group variables (list, create, update, delete)
16
- - **Issues** — list, get, create, update, add comments
17
- - **Groups** — list, get, share/unshare groups
18
-
19
- ## Common Workflows
20
-
21
- - **Code review**: `list_mrs` -> `mr_changes` -> `list_mr_discussions` -> `add_mr_note` or `create_mr_discussion` -> `resolve_discussion`
22
- - **Pipeline debugging**: `list_pipelines` -> `get_pipeline` -> `get_job_log` -> `retry_job`
23
- - **Release process**: `list_commits` -> `compare` -> `create_tag` -> `create_release`
24
- - **Branch protection**: `list_project_approval_rules` -> `create_project_approval_rule` -> `update_project_merge_settings`
25
- - **Issue triage**: `list_issues` -> `get_issue` -> `update_issue` -> `add_issue_comment`
26
-
27
- ## Notes
28
-
29
- - Set `GITLAB_READ_ONLY=true` to restrict all operations to read-only (no writes, no deletes).
30
- - Token requires appropriate GitLab scopes: `api` for full access, `read_api` for read-only.
31
- - Default request timeout is 30 seconds; override with `GITLAB_TIMEOUT`.
32
- - SSL verification is on by default; disable with `GITLAB_SSL_VERIFY=false` for self-signed certs.
33
- - Works with GitLab.com and self-hosted GitLab instances.
File without changes
File without changes
File without changes
File without changes