GitLab MCP Implementation Plan
This report breaks down the work to build a GitLab MCP server. It covers tasks, acceptance criteria, difficulty estimates, schedule, and alternatives (using an existing server if discovered).
---
Decision Gate: Build vs Adopt
- During research, no mature existing MCP server for GitLab was found. If one appears later, we can reassess.
- Current path: Build from scratch in Go (static binary, easy deployment), with optional web transport.
---
Task Breakdown
T1: Project Bootstrap
- Create Go module: `go mod init github.com/openclaw/gitlab-mcp`
- Add dependencies:
- MCP library: either `github.com/modelcontextprotocol/go-sdk` (if stable) or a minimal JSON‑RPC stdio implementation we write.
- `golang.org/x/time/rate` for rate limiting.
- `golang.org/x/net/publicsuffix`? Not needed.
- Create basic `main.go` with flag parsing (`--web`, `--port`, `--token`).
- Load environment: `GITLAB_URL`, `GITLAB_TOKEN`.
- Acceptance: Binary builds; reads env and flags.
- Difficulty: S (0.5 d)
T2: GitLab API Client
- Define Go structs for request/response payloads (marshal/unmarshal JSON). No need for full coverage; only fields used by tools.
- Implement `Client` with methods:
- `Get(url string, dest interface{}) error`
- `Post(url string, data interface{}, dest interface{}) error`
- `Put(url string, data interface{}, dest interface{}) error`
- `Delete(url string, dest interface{}) error`
- Add `SetToken(token string)`.
- Implement helpers:
- `projectPath(project string) string` – URL‑encode slashes.
- `listAllPages` helper for paginated endpoints (append `?per_page=100&page=N` until empty).
- Add rate limiter (`rate.NewLimiter(2, 1)` – 2 QPS).
- Add retry logic (3 retries with backoff on network errors or 429).
- Acceptance: Can call `GET /projects` and `POST /projects/:id/issues` with test token.
- Difficulty: M (1 d)
T3: MCP Server Skeleton
- If using MCP SDK: initialize server, register tools.
- If custom: implement JSON‑RPC 2.0 over stdin/stdout:
- Read lines → accumulate JSON objects.
- Handle `initialize` (capabilities negotiation).
- For each `method`, dispatch to handler.
- Write response with `jsonrpc`, `id`, `result` or `error`.
- Also implement `--web` variant: start HTTP server with `POST /mcp` that uses same dispatcher.
- Add optional `--token` flag for web mode: verify `Authorization: Bearer <token>` header.
- Acceptance: Can receive a call `gitlab.list_projects` (stub) and return a result.
- Difficulty: M (1 d)
T4: Implement Project Tools
- `gitlab.list_projects`:
- Call `GET /projects` with query params `owned`, `search`, `membership`? We’ll use `?owned=true` if param set; `?search=...` if given; also support `?min_access_level=30`? Not needed.
- Parse list; return simplified objects.
- `gitlab.get_project`:
- Call `GET /projects/:id` where `:id` is URL‑encoded path or numeric.
- Acceptance: Successfully list owned projects and fetch details.
- Difficulty: S (0.5 d)
T5: Implement Issue Tools
- `gitlab.list_issues`:
- Build query: `GET /projects/:id/issues` with `state`, `labels`, `assignee_id`, `milestone`, `sort`.
- Parse list; return simplified issue objects.
- `gitlab.create_issue`:
- `POST /projects/:id/issues` with `title`, `description`, `assignee_ids`, `labels`, `milestone_id`.
- Return `iid` and `web_url`.
- `gitlab.update_issue`:
- `PUT /projects/:id/issues/:issue_iid` with partial fields (only provided ones).
- `gitlab.close_issue`:
- `PUT /projects/:id/issues/:issue_iid` with `{ "state_event": "close" }`.
- Acceptance: Create, list, close issues for a test project.
- Difficulty: M (1 d)
T6: Implement Merge Request Tools
- `gitlab.list_merge_requests`:
- `GET /projects/:id/merge_requests` with `state`, `target_branch`, `source_branch`, `assignee_id`.
- `gitlab.create_merge_request`:
- `POST /projects/:id/merge_requests` with source, target, title, description, assignee_id, labels, remove_source_branch.
- `gitlab.merge_merge_request`:
- `PUT /projects/:id/merge_requests/:mr_iid/merge` with query params `merge_when_ready`, `squash`, `should_remove_source_branch`.
- `gitlab.approve_merge_request`:
- `POST /projects/:id/merge_requests/:mr_iid/approve` (requires `Merge Requests` permission).
- Acceptance: Create MR, approve, merge.
- Difficulty: M (1 d)
T7: Repository File Tools
- `gitlab.upsert_file`:
- First call `GET /projects/:id/repository/files/:file_path` with `ref=branch` to get current `sha` (if exists).
- If file exists: `PUT /projects/:id/repository/files/:file_path` with `branch`, `content`, `commit_message`, and `sha`.
- If not: `POST /projects/:id/repository/files/:file_path` with same body minus `sha`.
- Encode `content` as base64; API expects `content` base64 string. We’ll accept plain text and encode internally.
- `gitlab.get_file`:
- `GET /projects/:id/repository/files/:file_path?ref=...` returns `content` (base64) and `encoding`. Decode to plain text.
- `gitlab.delete_file`:
- `DELETE /projects/:id/repository/files/:file_path` with `branch`, `commit_message`, and `sha` (must provide current SHA; we can fetch first or require client to pass `sha`? Simpler: we can include a `sha` param; but `upsert_file` knows it. For a standalone delete, we need to fetch current `sha` then delete. So implement: `GET` file to obtain `sha`, then `DELETE` with `sha`. This is two calls but atomic enough given small scope.
- Acceptance: Create a file, read it, update it, delete it.
- Difficulty: M (1 d)
T8: CI/CD Tools
- `gitlab.list_pipelines`:
- `GET /projects/:id/pipelines` with `ref`, `status`, `order_by`, `sort`.
- `gitlab.trigger_pipeline`:
- `POST /projects/:id/pipeline` with `ref` and optional `variables` (key/value pairs).
- `gitlab.list_pipeline_jobs`:
- `GET /projects/:id/pipelines/:pipeline_id/jobs`.
- `gitlab.play_job`:
- `POST /projects/:id/jobs/:job_id/play` (requires manual job).
- Acceptance: Trigger a pipeline on a branch and list its jobs.
- Difficulty: S (0.5 d)
T9: Error Handling & Logging
- Centralize error parsing: GitLab returns JSON with `message` and sometimes `error`. Map to MCP error codes.
- On HTTP non‑2xx, read body, unmarshal into `{message: string}`, return MCP error with code = HTTP status.
- Log errors to stderr without sensitive data (mask token).
- Acceptance: Clear errors returned to client; no token leakage.
- Difficulty: S (0.25 d)
T10: Rate Limiting & Retries
- Apply `rate.Limiter` to each API call (token bucket).
- Wrap calls with retry logic: on network error or 429/5xx, wait exponential backoff (e.g., 500ms, 1s, 2s) up to 3 attempts.
- If 429 with `Retry-After` header, respect it.
- Acceptance: Burst of calls throttled; transient failures auto‑retried.
- Difficulty: S (0.25 d)
T11: Testing
- Unit tests for each tool using `httptest.NewServer` that mimics GitLab API responses (sample JSON fixtures).
- Test pagination handling (multiple pages).
- Test error paths (404, 403, 429).
- Integration tests against a real GitLab sandbox or a local self‑hosted instance (e.g., using Docker `gitlab/gitlab-runner`? Hard; maybe use GitLab.com test repo).
- Example test: create issue in a throwaway project, read back, delete.
- Acceptance: `go test ./...` passes; coverage >70%.
- Difficulty: M (1.5 d)
T12: Documentation & Packaging
- README.md:
- Overview, features.
- Prerequisites: Go or download binary.
- Environment variables: `GITLAB_URL`, `GITLAB_TOKEN`.
- Usage: `gitlab-mcp` (stdio) or `gitlab-mcp --web --port 8080`.
- Tool reference (tables).
- Security: token scopes recommended (`api`, `read_repository`, `write_repository`).
- Troubleshooting.
- Build: `make build` (produces binary).
- Optional: systemd unit file.
- Acceptance: Developer can build, run, and understand tool set.
- Difficulty: S (0.5 d)
---
Effort Estimate Summary
| Task | Difficulty | Duration |
|------|------------|----------|
| T1 | S | 0.5 d |
| T2 | M | 1 d |
| T3 | M | 1 d |
| T4 | S | 0.5 d |
| T5 | M | 1 d |
| T6 | M | 1 d |
| T7 | M | 1 d |
| T8 | S | 0.5 d |
| T9 | S | 0.25 d |
| T10 | S | 0.25 d |
| T11 | M | 1.5 d |
| T12 | S | 0.5 d |
| Total | | ~10–11 days (plus buffer → 14 days) |
---
Milestones
- M1 (Day 2): API client + basic projects.list works.
- M2 (Day 4): Issues and MR tools functional.
- M3 (Day 6): File operations and CI tools done.
- M4 (Day 8): Error handling, rate limiting, retries.
- M5 (Day 10): Unit tests passing.
- M6 (Day 12): Documentation and release candidate.
---
Risks & Mitigations
- API rate limits: GitLab has limits; use per‑request `X-RateLimit-Remaining`? We’ll implement conservative limiter (2 QPS) and respect 429.
- Large responses/pagination: Projects with thousands of issues may require multiple pages; we implement pagination helper that fetches all pages for list tools, or limit with `?per_page=100` and warn.
- Authentication scopes: Some tools need different scopes. Document minimum per tool.
- Branch existence: `upsert_file` requires branch to exist; we could auto‑create branch from default? Might be extra. Keep simple: error if branch not found.
- Base64 handling: Ensure correct encoding/decoding; treat result as UTF‑8 text.
---
Alternatives
- Use existing MCP server: if `mcp-server-gitlab` appears, evaluate and adopt. Could save 2 weeks.
- GraphQL variant: later replace REST client with GraphQL for efficiency, but initial REST is fine.
---
Word count: ~1,050