mcp-gitlabpr 1.0.0__tar.gz → 1.0.2__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.
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/.gitignore +2 -1
- mcp_gitlabpr-1.0.2/PKG-INFO +83 -0
- mcp_gitlabpr-1.0.2/README.md +70 -0
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/mcp_gitlabpr/server.py +85 -46
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/pyproject.toml +3 -3
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/tests/test_server.py +81 -17
- mcp_gitlabpr-1.0.0/PKG-INFO +0 -79
- mcp_gitlabpr-1.0.0/README.md +0 -66
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/LICENSE +0 -0
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/mcp_gitlabpr/__init__.py +0 -0
- {mcp_gitlabpr-1.0.0 → mcp_gitlabpr-1.0.2}/mcp_gitlabpr/config.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-gitlabpr
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: MCP server para merge requests do GitLab (API v4, CE 10.8+): listar, obter, criar e mesclar, só no projeto do workspace
|
|
5
|
+
Project-URL: Repository, https://github.com/dxaraujo/mcp-gitlabpr
|
|
6
|
+
Author-email: Daniel Xavier Araújo <danielxaraujo@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: mcp[cli]>=2.2
|
|
11
|
+
Requires-Dist: requests>=2.31
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# mcp-gitlabpr
|
|
15
|
+
|
|
16
|
+
Servidor MCP para **merge requests** do GitLab (o "PR" do GitLab): listar, obter (com diffs), criar e mesclar. Usa a API REST
|
|
17
|
+
v4 com personal access token e só os parâmetros que já existem na **GitLab CE 10.8**
|
|
18
|
+
([doc da 10.8.7](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/v10.8.7/doc/api/merge_requests.md)), então também
|
|
19
|
+
funciona em versões mais novas.
|
|
20
|
+
|
|
21
|
+
## Credenciais
|
|
22
|
+
|
|
23
|
+
| SO | Arquivo |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Linux/Mac | `~/.config/mcp-gitlabpr/gitlabpr.properties` |
|
|
26
|
+
| Windows | `%APPDATA%\mcp-gitlabpr\gitlabpr.properties` |
|
|
27
|
+
|
|
28
|
+
Para usar outro caminho, defina a variável de ambiente `MCP_GITLABPR_CONFIG`.
|
|
29
|
+
|
|
30
|
+
```ini
|
|
31
|
+
[DEFAULT]
|
|
32
|
+
url = https://gitlab.SEU-SERVIDOR
|
|
33
|
+
token = SEU_PERSONAL_ACCESS_TOKEN
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `PRIVATE-TOKEN`.
|
|
37
|
+
|
|
38
|
+
**Certificados:** o MCP confia nas CAs da máquina e, se existir, em `gitlabpr-bundle.pem` na mesma pasta do
|
|
39
|
+
`gitlabpr.properties` (cadeia PEM do servidor). `REQUESTS_CA_BUNDLE` também funciona.
|
|
40
|
+
|
|
41
|
+
## Instalação
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"gitlabpr": {
|
|
47
|
+
"type": "stdio",
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["mcp-gitlabpr@latest"],
|
|
50
|
+
"env": { "UV_SYSTEM_CERTS": "true" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Desenvolvimento:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync
|
|
60
|
+
uv run pytest
|
|
61
|
+
uv run mcp dev mcp_gitlabpr/server.py # MCP Inspector
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tools
|
|
65
|
+
|
|
66
|
+
As tools só trabalham no **projeto do workspace**: não há como consultar outro projeto nem MRs de todos os projetos.
|
|
67
|
+
O projeto vem do remote do git (`origin`, ou o primeiro remote; aceita `https://`, `ssh://` e `git@host:`) da `pasta`
|
|
68
|
+
informada, o caminho absoluto da raiz do workspace, e `origem` vem da branch atual dela. O agente deve passar `pasta`
|
|
69
|
+
sempre: o Kiro não inicia o MCP de um power na pasta do workspace, e sem `pasta` vale o cwd do processo. Fora de um
|
|
70
|
+
repositório, a pessoa cola a URL do projeto no GitLab (ex.: `https://gitlab.SEU-SERVIDOR/grupo/projeto`; a URL de um MR do
|
|
71
|
+
projeto também serve) e o agente a passa em `url_projeto`; aí `origem` é obrigatória no `gitlab_criar_mr`. O host do
|
|
72
|
+
remote ou da URL precisa ser o do `gitlabpr.properties`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
|
|
73
|
+
da tela). Autor e responsável são usernames: o MCP os converte em id por `GET /users?username=`, porque a 10.8 só
|
|
74
|
+
filtra por `author_id`/`assignee_id`.
|
|
75
|
+
|
|
76
|
+
| Tool | Para quê |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `gitlab_listar_mrs(pasta?, url_projeto?, estado=opened, busca?, origem?, destino?, autor?, responsavel?, limite≤100, pagina=1)` | MRs do projeto. Devolve `total`, `proxima_pagina` e os itens resumidos |
|
|
79
|
+
| `gitlab_obter_mr(iid, pasta?, url_projeto?, diffs=false)` | Detalhes do MR (descrição, labels, conflitos, merge). Com `diffs`, os arquivos alterados e o diff de cada um, cortado em 4000 caracteres |
|
|
80
|
+
| `gitlab_criar_mr(titulo, destino, pasta?, url_projeto?, origem?, descricao?, responsavel?, labels?, remover_origem=true)` | Cria o MR, já com "apagar a branch de origem após o merge" marcado (passe `remover_origem=false` para manter a branch); a branch de origem precisa já estar no GitLab (`git push`). Se já existe um MR aberto entre as mesmas branches, o GitLab responde 409 |
|
|
81
|
+
| `gitlab_mesclar_mr(iid, pasta?, url_projeto?, sha?, remover_origem?, mensagem?)` | Faz o merge de um MR aberto; recusa antes de chamar o GitLab se ele está em WIP, tem conflito ou não é mesclável. Com `sha`, o GitLab recusa (409) se a branch recebeu commits depois desse commit |
|
|
82
|
+
|
|
83
|
+
`estado` aceita `opened`, `closed`, `merged` ou `all`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# mcp-gitlabpr
|
|
2
|
+
|
|
3
|
+
Servidor MCP para **merge requests** do GitLab (o "PR" do GitLab): listar, obter (com diffs), criar e mesclar. Usa a API REST
|
|
4
|
+
v4 com personal access token e só os parâmetros que já existem na **GitLab CE 10.8**
|
|
5
|
+
([doc da 10.8.7](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/v10.8.7/doc/api/merge_requests.md)), então também
|
|
6
|
+
funciona em versões mais novas.
|
|
7
|
+
|
|
8
|
+
## Credenciais
|
|
9
|
+
|
|
10
|
+
| SO | Arquivo |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Linux/Mac | `~/.config/mcp-gitlabpr/gitlabpr.properties` |
|
|
13
|
+
| Windows | `%APPDATA%\mcp-gitlabpr\gitlabpr.properties` |
|
|
14
|
+
|
|
15
|
+
Para usar outro caminho, defina a variável de ambiente `MCP_GITLABPR_CONFIG`.
|
|
16
|
+
|
|
17
|
+
```ini
|
|
18
|
+
[DEFAULT]
|
|
19
|
+
url = https://gitlab.SEU-SERVIDOR
|
|
20
|
+
token = SEU_PERSONAL_ACCESS_TOKEN
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `PRIVATE-TOKEN`.
|
|
24
|
+
|
|
25
|
+
**Certificados:** o MCP confia nas CAs da máquina e, se existir, em `gitlabpr-bundle.pem` na mesma pasta do
|
|
26
|
+
`gitlabpr.properties` (cadeia PEM do servidor). `REQUESTS_CA_BUNDLE` também funciona.
|
|
27
|
+
|
|
28
|
+
## Instalação
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"gitlabpr": {
|
|
34
|
+
"type": "stdio",
|
|
35
|
+
"command": "uvx",
|
|
36
|
+
"args": ["mcp-gitlabpr@latest"],
|
|
37
|
+
"env": { "UV_SYSTEM_CERTS": "true" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Desenvolvimento:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync
|
|
47
|
+
uv run pytest
|
|
48
|
+
uv run mcp dev mcp_gitlabpr/server.py # MCP Inspector
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
As tools só trabalham no **projeto do workspace**: não há como consultar outro projeto nem MRs de todos os projetos.
|
|
54
|
+
O projeto vem do remote do git (`origin`, ou o primeiro remote; aceita `https://`, `ssh://` e `git@host:`) da `pasta`
|
|
55
|
+
informada, o caminho absoluto da raiz do workspace, e `origem` vem da branch atual dela. O agente deve passar `pasta`
|
|
56
|
+
sempre: o Kiro não inicia o MCP de um power na pasta do workspace, e sem `pasta` vale o cwd do processo. Fora de um
|
|
57
|
+
repositório, a pessoa cola a URL do projeto no GitLab (ex.: `https://gitlab.SEU-SERVIDOR/grupo/projeto`; a URL de um MR do
|
|
58
|
+
projeto também serve) e o agente a passa em `url_projeto`; aí `origem` é obrigatória no `gitlab_criar_mr`. O host do
|
|
59
|
+
remote ou da URL precisa ser o do `gitlabpr.properties`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
|
|
60
|
+
da tela). Autor e responsável são usernames: o MCP os converte em id por `GET /users?username=`, porque a 10.8 só
|
|
61
|
+
filtra por `author_id`/`assignee_id`.
|
|
62
|
+
|
|
63
|
+
| Tool | Para quê |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `gitlab_listar_mrs(pasta?, url_projeto?, estado=opened, busca?, origem?, destino?, autor?, responsavel?, limite≤100, pagina=1)` | MRs do projeto. Devolve `total`, `proxima_pagina` e os itens resumidos |
|
|
66
|
+
| `gitlab_obter_mr(iid, pasta?, url_projeto?, diffs=false)` | Detalhes do MR (descrição, labels, conflitos, merge). Com `diffs`, os arquivos alterados e o diff de cada um, cortado em 4000 caracteres |
|
|
67
|
+
| `gitlab_criar_mr(titulo, destino, pasta?, url_projeto?, origem?, descricao?, responsavel?, labels?, remover_origem=true)` | Cria o MR, já com "apagar a branch de origem após o merge" marcado (passe `remover_origem=false` para manter a branch); a branch de origem precisa já estar no GitLab (`git push`). Se já existe um MR aberto entre as mesmas branches, o GitLab responde 409 |
|
|
68
|
+
| `gitlab_mesclar_mr(iid, pasta?, url_projeto?, sha?, remover_origem?, mensagem?)` | Faz o merge de um MR aberto; recusa antes de chamar o GitLab se ele está em WIP, tem conflito ou não é mesclável. Com `sha`, o GitLab recusa (409) se a branch recebeu commits depois desse commit |
|
|
69
|
+
|
|
70
|
+
`estado` aceita `opened`, `closed`, `merged` ou `all`.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""Servidor MCP para merge requests do GitLab: listar, obter (com diffs) e
|
|
1
|
+
"""Servidor MCP para merge requests do GitLab: listar, obter (com diffs), criar e mesclar.
|
|
2
2
|
|
|
3
3
|
API REST v4 com personal access token (header PRIVATE-TOKEN, escopo `api`). Só usa parâmetros que já existem na
|
|
4
4
|
GitLab CE 10.8: autor e responsável vão por id (`author_username` é de versões novas), resolvidos por username.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Só trabalha no projeto do repositório git do workspace (`pasta`; sem ela, a pasta em que o MCP roda), pelo remote
|
|
7
|
+
origin ou o primeiro remote. Fora de um repositório, a pessoa cola a URL do projeto no GitLab (`url_projeto`). O host
|
|
8
|
+
precisa ser o do gitlabpr.properties.
|
|
7
9
|
"""
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
@@ -24,11 +26,13 @@ from .config import GitlabConfig, default_path, load_config
|
|
|
24
26
|
mcp = MCPServer(
|
|
25
27
|
"gitlabpr",
|
|
26
28
|
instructions=(
|
|
27
|
-
"Merge requests (MR, o 'PR' do GitLab)
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"projeto
|
|
31
|
-
"
|
|
29
|
+
"Merge requests (MR, o 'PR' do GitLab) só do projeto em que se está trabalhando: ele vem do remote do "
|
|
30
|
+
"repositório git em `pasta` e `origem` vem da branch atual dele. Informe sempre `pasta` com o caminho absoluto "
|
|
31
|
+
"da raiz do workspace. Se a tool disser que não achou o projeto, peça à pessoa a URL do projeto no GitLab "
|
|
32
|
+
"(ex.: https://gitlab/grupo/projeto) e informe-a em `url_projeto`; nunca a deduza nem a invente. `iid` é o "
|
|
33
|
+
"número do MR dentro do projeto (o !123 "
|
|
34
|
+
"da tela). Autor e responsável são usernames. Liste com gitlab_listar_mrs, detalhe (e veja os diffs) com "
|
|
35
|
+
"gitlab_obter_mr, crie com gitlab_criar_mr e faça o merge com gitlab_mesclar_mr."
|
|
32
36
|
),
|
|
33
37
|
)
|
|
34
38
|
|
|
@@ -113,10 +117,12 @@ def set_session(session: GitlabSession | None) -> None:
|
|
|
113
117
|
|
|
114
118
|
# ---------------------------------------------------------------- helpers
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
def _git(pasta: str | None, *args: str) -> str | None:
|
|
121
|
+
"""git na `pasta` do workspace; sem ela, no cwd do processo MCP (o Kiro não inicia o de um power no workspace)."""
|
|
122
|
+
if pasta and not Path(pasta).is_dir():
|
|
123
|
+
raise ValueError(f"`pasta` não existe: {pasta}. Informe o caminho absoluto da raiz do workspace.")
|
|
118
124
|
try:
|
|
119
|
-
r = subprocess.run(["git", *args], capture_output=True, text=True, timeout=10)
|
|
125
|
+
r = subprocess.run(["git", *args], capture_output=True, text=True, timeout=10, cwd=pasta or None)
|
|
120
126
|
except (OSError, subprocess.TimeoutExpired):
|
|
121
127
|
return None
|
|
122
128
|
return (r.stdout.strip() or None) if r.returncode == 0 else None
|
|
@@ -124,9 +130,13 @@ def _git(*args: str) -> str | None:
|
|
|
124
130
|
|
|
125
131
|
def _caminho_remoto(url: str) -> str:
|
|
126
132
|
"""'grupo/projeto' de https://host[/prefixo]/g/p.git, ssh://git@host:22/g/p.git ou git@host:g/p.git."""
|
|
133
|
+
gitlab = urlsplit(get_session().cfg.url)
|
|
134
|
+
host = urlsplit(url).hostname if "://" in url else url.partition(":")[0].rpartition("@")[2]
|
|
135
|
+
if (host or "").lower() != (gitlab.hostname or "").lower():
|
|
136
|
+
raise ValueError(f"O remote do repositório ({url}) não é do GitLab configurado ({get_session().cfg.url}).")
|
|
127
137
|
if "://" in url:
|
|
128
138
|
path = urlsplit(url).path
|
|
129
|
-
prefixo =
|
|
139
|
+
prefixo = gitlab.path.rstrip("/") # GitLab sob subcaminho (https://host/gitlab)
|
|
130
140
|
if prefixo and path.startswith(prefixo + "/") and url.startswith(("http://", "https://")):
|
|
131
141
|
path = path[len(prefixo):]
|
|
132
142
|
else:
|
|
@@ -135,22 +145,26 @@ def _caminho_remoto(url: str) -> str:
|
|
|
135
145
|
return path[:-4] if path.endswith(".git") else path
|
|
136
146
|
|
|
137
147
|
|
|
138
|
-
def _projeto(
|
|
139
|
-
"""'grupo/projeto'
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
url =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
def _projeto(pasta: str | None, url_projeto: str | None = None) -> str:
|
|
149
|
+
"""'grupo/projeto' da URL colada pela pessoa ou do remote do git, URL-encoded para o path (grupo%2Fprojeto)."""
|
|
150
|
+
if url_projeto:
|
|
151
|
+
url = url_projeto.strip()
|
|
152
|
+
if not url.startswith(("https://", "http://")):
|
|
153
|
+
raise ValueError(f"`url_projeto` precisa ser a URL do projeto no GitLab (https://...): {url}")
|
|
154
|
+
url = url.split("/-/")[0].split("/merge_requests")[0] # aceita a URL de um MR ou de uma página do projeto
|
|
155
|
+
return quote(_caminho_remoto(url), safe="")
|
|
156
|
+
remotes = (_git(pasta, "remote") or "").split()
|
|
157
|
+
remote = "origin" if "origin" in remotes else (remotes[0] if remotes else None)
|
|
158
|
+
url = remote and _git(pasta, "remote", "get-url", remote)
|
|
159
|
+
if not url:
|
|
160
|
+
raise ValueError(f"Projeto não encontrado: {pasta or Path.cwd()} não é um repositório git com remote. Informe "
|
|
161
|
+
"em `pasta` a raiz do workspace ou peça à pessoa a URL do projeto no GitLab e informe-a em "
|
|
162
|
+
"`url_projeto`.")
|
|
163
|
+
return quote(_caminho_remoto(url), safe="")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _branch_atual(pasta: str | None) -> str:
|
|
167
|
+
branch = _git(pasta, "rev-parse", "--abbrev-ref", "HEAD")
|
|
154
168
|
if not branch or branch == "HEAD":
|
|
155
169
|
raise ValueError("Sem branch atual no git (fora de um repositório ou HEAD destacado): pergunte ao usuário a "
|
|
156
170
|
"branch de origem e informe-a em `origem`.")
|
|
@@ -190,12 +204,12 @@ def _resumo(mr: dict) -> dict:
|
|
|
190
204
|
# ---------------------------------------------------------------- tools
|
|
191
205
|
|
|
192
206
|
@tool
|
|
193
|
-
def gitlab_listar_mrs(
|
|
207
|
+
def gitlab_listar_mrs(pasta: str | None = None, url_projeto: str | None = None, estado: str = "opened",
|
|
194
208
|
busca: str | None = None, origem: str | None = None, destino: str | None = None,
|
|
195
209
|
autor: str | None = None, responsavel: str | None = None, limite: int = 20,
|
|
196
210
|
pagina: int = 1) -> dict:
|
|
197
|
-
"""Lista merge requests
|
|
198
|
-
|
|
211
|
+
"""Lista merge requests do projeto do repositório git em `pasta` (caminho absoluto da raiz do workspace) ou
|
|
212
|
+
de `url_projeto` (a URL do projeto no GitLab, colada pela pessoa).
|
|
199
213
|
|
|
200
214
|
estado: opened (padrão), closed, merged ou all. busca: texto no título/descrição.
|
|
201
215
|
origem/destino: branches. autor/responsavel: usernames. limite ≤ 100 por página.
|
|
@@ -214,11 +228,7 @@ def gitlab_listar_mrs(projeto: str | None = None, todos_projetos: bool = False,
|
|
|
214
228
|
params["author_id"] = _usuario_id(autor)
|
|
215
229
|
if responsavel:
|
|
216
230
|
params["assignee_id"] = _usuario_id(responsavel)
|
|
217
|
-
|
|
218
|
-
path, params["scope"] = "/merge_requests", "all" # padrão do endpoint global é created-by-me
|
|
219
|
-
else:
|
|
220
|
-
path = f"/projects/{_projeto(projeto)}/merge_requests"
|
|
221
|
-
resp = get_session().request("GET", path, params=params)
|
|
231
|
+
resp = get_session().request("GET", f"/projects/{_projeto(pasta, url_projeto)}/merge_requests", params=params)
|
|
222
232
|
return {
|
|
223
233
|
"total": int(resp.headers["X-Total"]) if resp.headers.get("X-Total") else None,
|
|
224
234
|
"pagina": params["page"],
|
|
@@ -228,10 +238,10 @@ def gitlab_listar_mrs(projeto: str | None = None, todos_projetos: bool = False,
|
|
|
228
238
|
|
|
229
239
|
|
|
230
240
|
@tool
|
|
231
|
-
def gitlab_obter_mr(iid: int,
|
|
232
|
-
"""Detalhes de um merge request (descrição, labels, merge)
|
|
241
|
+
def gitlab_obter_mr(iid: int, pasta: str | None = None, url_projeto: str | None = None, diffs: bool = False) -> dict:
|
|
242
|
+
"""Detalhes de um merge request do projeto do repositório git em `pasta` (descrição, labels, merge).
|
|
233
243
|
Com diffs=True, inclui os arquivos alterados e o diff de cada um (cortado em 4000 caracteres)."""
|
|
234
|
-
base = f"/projects/{_projeto(
|
|
244
|
+
base = f"/projects/{_projeto(pasta, url_projeto)}/merge_requests/{int(iid)}"
|
|
235
245
|
mr = get_session().request("GET", base + ("/changes" if diffs else "")).json()
|
|
236
246
|
out = _resumo(mr) | {
|
|
237
247
|
"descricao": mr.get("description"),
|
|
@@ -255,15 +265,17 @@ def gitlab_obter_mr(iid: int, projeto: str | None = None, diffs: bool = False) -
|
|
|
255
265
|
|
|
256
266
|
|
|
257
267
|
@tool
|
|
258
|
-
def gitlab_criar_mr(titulo: str, destino: str,
|
|
259
|
-
|
|
260
|
-
remover_origem: bool =
|
|
261
|
-
"""Cria um merge request de `origem` (padrão: branch atual do git) para `destino` no
|
|
262
|
-
|
|
263
|
-
remover_origem:
|
|
268
|
+
def gitlab_criar_mr(titulo: str, destino: str, pasta: str | None = None, url_projeto: str | None = None,
|
|
269
|
+
origem: str | None = None, descricao: str | None = None, responsavel: str | None = None,
|
|
270
|
+
labels: list[str] | None = None, remover_origem: bool = True) -> dict:
|
|
271
|
+
"""Cria um merge request de `origem` (padrão: branch atual do git) para `destino` no projeto do repositório
|
|
272
|
+
git em `pasta`. A branch de origem precisa estar no GitLab (git push). responsavel: username.
|
|
273
|
+
remover_origem (padrão: sim): o MR já nasce com 'apagar a branch de origem após o merge' marcado; passe False para manter a branch. Falha (HTTP 409) se já existe MR aberto entre as branches."""
|
|
264
274
|
if not titulo.strip():
|
|
265
275
|
raise ValueError("Informe `titulo`.")
|
|
266
|
-
|
|
276
|
+
if url_projeto and not origem:
|
|
277
|
+
raise ValueError("Com `url_projeto`, pergunte ao usuário a branch de origem e informe-a em `origem`.")
|
|
278
|
+
data = {"source_branch": origem or _branch_atual(pasta), "target_branch": destino, "title": titulo,
|
|
267
279
|
"remove_source_branch": remover_origem}
|
|
268
280
|
if descricao:
|
|
269
281
|
data["description"] = descricao
|
|
@@ -271,10 +283,37 @@ def gitlab_criar_mr(titulo: str, destino: str, origem: str | None = None, projet
|
|
|
271
283
|
data["assignee_id"] = _usuario_id(responsavel)
|
|
272
284
|
if labels:
|
|
273
285
|
data["labels"] = ",".join(labels)
|
|
274
|
-
mr = get_session().request("POST", f"/projects/{_projeto(
|
|
286
|
+
mr = get_session().request("POST", f"/projects/{_projeto(pasta, url_projeto)}/merge_requests", json=data).json()
|
|
275
287
|
return _resumo(mr)
|
|
276
288
|
|
|
277
289
|
|
|
290
|
+
@tool
|
|
291
|
+
def gitlab_mesclar_mr(iid: int, pasta: str | None = None, url_projeto: str | None = None, sha: str | None = None,
|
|
292
|
+
remover_origem: bool | None = None, mensagem: str | None = None) -> dict:
|
|
293
|
+
"""Faz o merge de um merge request aberto do projeto do repositório git em `pasta`. Antes confere que ele não
|
|
294
|
+
está em WIP, não tem conflito e que o GitLab o considera mesclável. sha: o commit aprovado; se a branch recebeu
|
|
295
|
+
commits depois, o GitLab recusa (HTTP 409). remover_origem: apaga a branch de origem (padrão: o que o MR já
|
|
296
|
+
define). mensagem: do commit de merge."""
|
|
297
|
+
base = f"/projects/{_projeto(pasta, url_projeto)}/merge_requests/{int(iid)}"
|
|
298
|
+
mr = get_session().request("GET", base).json()
|
|
299
|
+
if mr.get("state") != "opened":
|
|
300
|
+
raise ValueError(f"MR !{iid} não está aberto (estado: {mr.get('state')}).")
|
|
301
|
+
if mr.get("work_in_progress"):
|
|
302
|
+
raise ValueError(f"MR !{iid} está em WIP/Draft: tire o prefixo antes do merge.")
|
|
303
|
+
if mr.get("has_conflicts") or mr.get("merge_status") == "cannot_be_merged":
|
|
304
|
+
raise ValueError(f"MR !{iid} tem conflito com {mr.get('target_branch')}: atualize a branch antes do merge.")
|
|
305
|
+
data = {}
|
|
306
|
+
if sha:
|
|
307
|
+
data["sha"] = sha
|
|
308
|
+
if remover_origem is not None:
|
|
309
|
+
data["should_remove_source_branch"] = remover_origem
|
|
310
|
+
if mensagem:
|
|
311
|
+
data["merge_commit_message"] = mensagem
|
|
312
|
+
mr = get_session().request("PUT", base + "/merge", json=data).json()
|
|
313
|
+
return _resumo(mr) | {"mesclado_por": _login(mr.get("merged_by")), "mesclado_em": mr.get("merged_at"),
|
|
314
|
+
"sha_merge": mr.get("merge_commit_sha")}
|
|
315
|
+
|
|
316
|
+
|
|
278
317
|
def main() -> None:
|
|
279
318
|
mcp.run()
|
|
280
319
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcp-gitlabpr"
|
|
3
|
-
version = "1.0.
|
|
4
|
-
description = "MCP server para merge requests do GitLab (API v4, CE 10.8+): listar, obter e
|
|
3
|
+
version = "1.0.2"
|
|
4
|
+
description = "MCP server para merge requests do GitLab (API v4, CE 10.8+): listar, obter, criar e mesclar, só no projeto do workspace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[project.urls]
|
|
16
|
-
Repository = "https://github.com/dxaraujo/
|
|
16
|
+
Repository = "https://github.com/dxaraujo/mcp-gitlabpr"
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
19
19
|
mcp-gitlabpr = "mcp_gitlabpr.server:main"
|
|
@@ -35,8 +35,17 @@ class FakeAdapter(BaseAdapter):
|
|
|
35
35
|
pass
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def git(*args):
|
|
39
|
+
subprocess.run(["git", *args], check=True, capture_output=True)
|
|
40
|
+
|
|
41
|
+
|
|
38
42
|
@pytest.fixture
|
|
39
|
-
def fake():
|
|
43
|
+
def fake(tmp_path_factory, monkeypatch):
|
|
44
|
+
"""Sessão falsa, rodando num repositório git cujo origin é o projeto g/p do GitLab de teste."""
|
|
45
|
+
repo = tmp_path_factory.mktemp("repo")
|
|
46
|
+
monkeypatch.chdir(repo)
|
|
47
|
+
git("init", "-q", "-b", "feat")
|
|
48
|
+
git("remote", "add", "origin", "git@gitlab.test:g/p.git")
|
|
40
49
|
adapter = FakeAdapter()
|
|
41
50
|
session = server.GitlabSession(GitlabConfig(url="https://gitlab.test", token="tok"))
|
|
42
51
|
session.session.mount("https://", adapter)
|
|
@@ -59,7 +68,7 @@ def test_config(tmp_path):
|
|
|
59
68
|
def test_listar_projeto(fake):
|
|
60
69
|
fake.routes[("GET", f"{API}/users")] = (200, [{"id": 42}])
|
|
61
70
|
fake.routes[("GET", f"{API}/projects/g%2Fp/merge_requests")] = (200, [MR], {"X-Total": "1"})
|
|
62
|
-
out = server.gitlab_listar_mrs(
|
|
71
|
+
out = server.gitlab_listar_mrs(autor="@joao", destino="dev", limite=500)
|
|
63
72
|
req = fake.calls[-1]
|
|
64
73
|
assert req.headers["PRIVATE-TOKEN"] == "tok"
|
|
65
74
|
assert "author_id=42" in req.url and "target_branch=dev" in req.url and "per_page=100" in req.url
|
|
@@ -67,40 +76,59 @@ def test_listar_projeto(fake):
|
|
|
67
76
|
assert out["itens"][0] | {} == server._resumo(MR) and out["itens"][0]["autor"] == "joao"
|
|
68
77
|
|
|
69
78
|
|
|
70
|
-
def
|
|
71
|
-
fake.routes[("GET", f"{API}/merge_requests")] = (200, [])
|
|
72
|
-
server.gitlab_listar_mrs(todos_projetos=True)
|
|
73
|
-
assert "scope=all" in fake.calls[-1].url
|
|
79
|
+
def test_listar_estado_invalido(fake):
|
|
74
80
|
with pytest.raises(ValueError):
|
|
75
|
-
server.gitlab_listar_mrs(
|
|
81
|
+
server.gitlab_listar_mrs(estado="aberto")
|
|
76
82
|
|
|
77
83
|
|
|
78
84
|
def test_obter_com_diffs(fake):
|
|
79
85
|
change = {"new_path": "a.py", "old_path": "a.py", "diff": "x" * 5000}
|
|
80
|
-
fake.routes[("GET", f"{API}/projects/
|
|
81
|
-
out = server.gitlab_obter_mr(7,
|
|
86
|
+
fake.routes[("GET", f"{API}/projects/g%2Fp/merge_requests/7/changes")] = (200, MR | {"changes": [change]})
|
|
87
|
+
out = server.gitlab_obter_mr(7, diffs=True)
|
|
82
88
|
assert out["arquivos"][0]["diff_cortado"] and len(out["arquivos"][0]["diff"]) == server.DIFF_MAX
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
def test_criar(fake):
|
|
86
92
|
fake.routes[("GET", f"{API}/users")] = (200, [{"id": 9}])
|
|
87
93
|
fake.routes[("POST", f"{API}/projects/g%2Fp/merge_requests")] = (201, MR)
|
|
88
|
-
out = server.gitlab_criar_mr("Ajuste", "dev", "feat",
|
|
94
|
+
out = server.gitlab_criar_mr("Ajuste", "dev", origem="feat", responsavel="maria", labels=["a", "b"])
|
|
89
95
|
assert json.loads(fake.calls[-1].body) == {"source_branch": "feat", "target_branch": "dev", "title": "Ajuste",
|
|
90
|
-
"remove_source_branch":
|
|
96
|
+
"remove_source_branch": True, "assignee_id": 9, "labels": "a,b"}
|
|
91
97
|
assert out["iid"] == 7
|
|
98
|
+
server.gitlab_criar_mr("Ajuste", "dev", origem="feat", remover_origem=False) # quem quiser manter a branch
|
|
99
|
+
assert json.loads(fake.calls[-1].body)["remove_source_branch"] is False
|
|
92
100
|
|
|
93
101
|
|
|
94
102
|
def test_erros(fake):
|
|
95
103
|
fake.routes[("GET", f"{API}/users")] = (200, [])
|
|
96
104
|
with pytest.raises(LookupError):
|
|
97
|
-
server.gitlab_criar_mr("t", "dev", "feat",
|
|
105
|
+
server.gitlab_criar_mr("t", "dev", origem="feat", responsavel="ninguem")
|
|
98
106
|
fake.routes[("POST", f"{API}/projects/g%2Fp/merge_requests")] = (409, {"message": ["Another open merge request"]})
|
|
99
107
|
with pytest.raises(RuntimeError, match="409"):
|
|
100
|
-
server.gitlab_criar_mr("t", "dev", "feat"
|
|
108
|
+
server.gitlab_criar_mr("t", "dev", origem="feat")
|
|
101
109
|
fake.routes[("GET", f"{API}/projects/g%2Fp/merge_requests/1")] = (401, {"message": "401 Unauthorized"})
|
|
102
110
|
with pytest.raises(RuntimeError, match="token"):
|
|
103
|
-
server.gitlab_obter_mr(1
|
|
111
|
+
server.gitlab_obter_mr(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_mesclar(fake):
|
|
115
|
+
base = f"{API}/projects/g%2Fp/merge_requests/7"
|
|
116
|
+
fake.routes[("GET", base)] = (200, MR | {"merge_status": "can_be_merged"})
|
|
117
|
+
fake.routes[("PUT", base + "/merge")] = (200, MR | {"state": "merged", "merged_by": {"username": "ana"},
|
|
118
|
+
"merge_commit_sha": "abc"})
|
|
119
|
+
out = server.gitlab_mesclar_mr(7, sha="f00", remover_origem=True)
|
|
120
|
+
assert json.loads(fake.calls[-1].body) == {"sha": "f00", "should_remove_source_branch": True}
|
|
121
|
+
assert out["estado"] == "merged" and out["mesclado_por"] == "ana" and out["sha_merge"] == "abc"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("extra, erro", [({"state": "merged"}, "aberto"), ({"work_in_progress": True}, "WIP"),
|
|
125
|
+
({"has_conflicts": True}, "conflito"),
|
|
126
|
+
({"merge_status": "cannot_be_merged"}, "conflito")])
|
|
127
|
+
def test_mesclar_recusa(fake, extra, erro):
|
|
128
|
+
fake.routes[("GET", f"{API}/projects/g%2Fp/merge_requests/7")] = (200, MR | extra)
|
|
129
|
+
with pytest.raises(ValueError, match=erro):
|
|
130
|
+
server.gitlab_mesclar_mr(7)
|
|
131
|
+
assert not any(c.method == "PUT" for c in fake.calls)
|
|
104
132
|
|
|
105
133
|
|
|
106
134
|
@pytest.mark.parametrize("url", ["https://gitlab.test/g/sub/p.git", "git@gitlab.test:g/sub/p.git",
|
|
@@ -114,13 +142,16 @@ def test_caminho_remoto_subcaminho(fake):
|
|
|
114
142
|
assert server._caminho_remoto("https://host/gitlab/g/p.git") == "g/p"
|
|
115
143
|
|
|
116
144
|
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
@pytest.mark.parametrize("url", ["https://github.com/g/p.git", "git@github.com:g/p.git",
|
|
146
|
+
"ssh://git@github.com/g/p.git"])
|
|
147
|
+
def test_remote_de_outro_host(fake, url):
|
|
148
|
+
with pytest.raises(ValueError, match="não é do GitLab configurado"):
|
|
149
|
+
server._caminho_remoto(url)
|
|
119
150
|
|
|
120
151
|
|
|
121
152
|
def test_projeto_e_branch_do_git(fake, tmp_path, monkeypatch):
|
|
122
153
|
monkeypatch.chdir(tmp_path)
|
|
123
|
-
with pytest.raises(ValueError, match="
|
|
154
|
+
with pytest.raises(ValueError, match="raiz do workspace"):
|
|
124
155
|
server.gitlab_listar_mrs()
|
|
125
156
|
with pytest.raises(ValueError, match="pergunte ao usuário a branch"):
|
|
126
157
|
server.gitlab_criar_mr("t", "dev")
|
|
@@ -130,3 +161,36 @@ def test_projeto_e_branch_do_git(fake, tmp_path, monkeypatch):
|
|
|
130
161
|
fake.routes[("POST", f"{API}/projects/g%2Fp/merge_requests")] = (201, MR)
|
|
131
162
|
server.gitlab_criar_mr("t", "dev")
|
|
132
163
|
assert json.loads(fake.calls[-1].body)["source_branch"] == "feat"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_pasta_do_workspace(fake, tmp_path, monkeypatch):
|
|
167
|
+
"""Com `pasta`, o projeto vem do repositório dela, não do cwd (o Kiro não inicia o MCP de um power no workspace)."""
|
|
168
|
+
repo = tmp_path / "ws"
|
|
169
|
+
repo.mkdir()
|
|
170
|
+
monkeypatch.chdir(repo)
|
|
171
|
+
git("init", "-q", "-b", "feat")
|
|
172
|
+
git("remote", "add", "origin", "https://gitlab.test/outro/ws.git")
|
|
173
|
+
monkeypatch.chdir(tmp_path)
|
|
174
|
+
fake.routes[("GET", f"{API}/projects/outro%2Fws/merge_requests")] = (200, [])
|
|
175
|
+
server.gitlab_listar_mrs(pasta=str(repo))
|
|
176
|
+
assert "/projects/outro%2Fws/" in fake.calls[-1].url
|
|
177
|
+
with pytest.raises(ValueError, match="não existe"):
|
|
178
|
+
server.gitlab_listar_mrs(pasta=str(tmp_path / "nada"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.parametrize("url", ["https://gitlab.test/g/p", "https://gitlab.test/g/p/", "https://gitlab.test/g/p.git",
|
|
182
|
+
"https://gitlab.test/g/p/merge_requests/7", "https://gitlab.test/g/p/-/tree/dev"])
|
|
183
|
+
def test_url_projeto_colada(fake, tmp_path, monkeypatch, url):
|
|
184
|
+
"""Fora de um repositório, a pessoa cola a URL do projeto; vale a de um MR ou de outra página dele."""
|
|
185
|
+
monkeypatch.chdir(tmp_path)
|
|
186
|
+
fake.routes[("GET", f"{API}/projects/g%2Fp/merge_requests")] = (200, [])
|
|
187
|
+
server.gitlab_listar_mrs(url_projeto=url)
|
|
188
|
+
assert "/projects/g%2Fp/merge_requests" in fake.calls[-1].url
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_url_projeto_recusada(fake):
|
|
192
|
+
for url in ("https://github.com/g/p", "g/p", "123"):
|
|
193
|
+
with pytest.raises(ValueError):
|
|
194
|
+
server.gitlab_listar_mrs(url_projeto=url)
|
|
195
|
+
with pytest.raises(ValueError, match="branch de origem"):
|
|
196
|
+
server.gitlab_criar_mr("t", "dev", url_projeto="https://gitlab.test/g/p")
|
mcp_gitlabpr-1.0.0/PKG-INFO
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: mcp-gitlabpr
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: MCP server para merge requests do GitLab (API v4, CE 10.8+): listar, obter e criar
|
|
5
|
-
Project-URL: Repository, https://github.com/dxaraujo/kiro/tree/main/mcps/gitlabpr
|
|
6
|
-
Author-email: Daniel Xavier Araújo <danielxaraujo@gmail.com>
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: mcp[cli]>=2.2
|
|
11
|
-
Requires-Dist: requests>=2.31
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
|
|
14
|
-
# mcp-gitlabpr
|
|
15
|
-
|
|
16
|
-
Servidor MCP para **merge requests** do GitLab (o "PR" do GitLab): listar, obter (com diffs) e criar. Usa a API REST
|
|
17
|
-
v4 com personal access token e só os parâmetros que já existem na **GitLab CE 10.8**
|
|
18
|
-
([doc da 10.8.7](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/v10.8.7/doc/api/merge_requests.md)), então também
|
|
19
|
-
funciona em versões mais novas.
|
|
20
|
-
|
|
21
|
-
## Credenciais
|
|
22
|
-
|
|
23
|
-
| SO | Arquivo |
|
|
24
|
-
|---|---|
|
|
25
|
-
| Linux/Mac | `~/.config/mcp-gitlabpr/gitlabpr.properties` |
|
|
26
|
-
| Windows | `%APPDATA%\mcp-gitlabpr\gitlabpr.properties` |
|
|
27
|
-
|
|
28
|
-
Para usar outro caminho, defina a variável de ambiente `MCP_GITLABPR_CONFIG`.
|
|
29
|
-
|
|
30
|
-
```ini
|
|
31
|
-
[DEFAULT]
|
|
32
|
-
url = https://gitlab.SEU-SERVIDOR
|
|
33
|
-
token = SEU_PERSONAL_ACCESS_TOKEN
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `PRIVATE-TOKEN`.
|
|
37
|
-
|
|
38
|
-
**Certificados:** o MCP confia nas CAs da máquina e, se existir, em `gitlabpr-bundle.pem` na mesma pasta do
|
|
39
|
-
`gitlabpr.properties` (cadeia PEM do servidor). `REQUESTS_CA_BUNDLE` também funciona.
|
|
40
|
-
|
|
41
|
-
## Instalação
|
|
42
|
-
|
|
43
|
-
```json
|
|
44
|
-
{
|
|
45
|
-
"mcpServers": {
|
|
46
|
-
"gitlabpr": {
|
|
47
|
-
"type": "stdio",
|
|
48
|
-
"command": "uvx",
|
|
49
|
-
"args": ["mcp-gitlabpr@latest"],
|
|
50
|
-
"env": { "UV_SYSTEM_CERTS": "true" }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Desenvolvimento:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
uv sync
|
|
60
|
-
uv run pytest
|
|
61
|
-
uv run mcp dev mcp_gitlabpr/server.py # MCP Inspector
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Tools
|
|
65
|
-
|
|
66
|
-
`projeto` é o caminho `grupo/projeto` (o MCP faz o URL-encode) ou o id numérico. **Sem `projeto`**, ele vem do remote
|
|
67
|
-
do git da pasta em que o MCP roda (`origin`, ou o primeiro remote; aceita `https://`, `ssh://` e `git@host:`), e
|
|
68
|
-
`origem` vem da branch atual. Fora de um repositório git a tool falha pedindo que o agente pergunte ao usuário. O
|
|
69
|
-
cliente MCP precisa iniciar o servidor na pasta do workspace; se não iniciar, informe `projeto`. `iid` é o número do MR no projeto
|
|
70
|
-
(o `!123` da tela). Autor e responsável são usernames: o MCP os converte em id por `GET /users?username=`, porque a 10.8
|
|
71
|
-
só filtra por `author_id`/`assignee_id`.
|
|
72
|
-
|
|
73
|
-
| Tool | Para quê |
|
|
74
|
-
|---|---|
|
|
75
|
-
| `gitlab_listar_mrs(projeto?, todos_projetos=false, estado=opened, busca?, origem?, destino?, autor?, responsavel?, limite≤100, pagina=1)` | MRs do projeto ou, com `todos_projetos`, de todos os visíveis (`scope=all`). Devolve `total`, `proxima_pagina` e os itens resumidos |
|
|
76
|
-
| `gitlab_obter_mr(iid, projeto?, diffs=false)` | Detalhes do MR (descrição, labels, conflitos, merge). Com `diffs`, os arquivos alterados e o diff de cada um, cortado em 4000 caracteres |
|
|
77
|
-
| `gitlab_criar_mr(titulo, destino, origem?, projeto?, descricao?, responsavel?, labels?, remover_origem=false)` | Cria o MR; a branch de origem precisa já estar no GitLab (`git push`). Se já existe um MR aberto entre as mesmas branches, o GitLab responde 409 |
|
|
78
|
-
|
|
79
|
-
`estado` aceita `opened`, `closed`, `merged` ou `all`.
|
mcp_gitlabpr-1.0.0/README.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# mcp-gitlabpr
|
|
2
|
-
|
|
3
|
-
Servidor MCP para **merge requests** do GitLab (o "PR" do GitLab): listar, obter (com diffs) e criar. Usa a API REST
|
|
4
|
-
v4 com personal access token e só os parâmetros que já existem na **GitLab CE 10.8**
|
|
5
|
-
([doc da 10.8.7](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/v10.8.7/doc/api/merge_requests.md)), então também
|
|
6
|
-
funciona em versões mais novas.
|
|
7
|
-
|
|
8
|
-
## Credenciais
|
|
9
|
-
|
|
10
|
-
| SO | Arquivo |
|
|
11
|
-
|---|---|
|
|
12
|
-
| Linux/Mac | `~/.config/mcp-gitlabpr/gitlabpr.properties` |
|
|
13
|
-
| Windows | `%APPDATA%\mcp-gitlabpr\gitlabpr.properties` |
|
|
14
|
-
|
|
15
|
-
Para usar outro caminho, defina a variável de ambiente `MCP_GITLABPR_CONFIG`.
|
|
16
|
-
|
|
17
|
-
```ini
|
|
18
|
-
[DEFAULT]
|
|
19
|
-
url = https://gitlab.SEU-SERVIDOR
|
|
20
|
-
token = SEU_PERSONAL_ACCESS_TOKEN
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `PRIVATE-TOKEN`.
|
|
24
|
-
|
|
25
|
-
**Certificados:** o MCP confia nas CAs da máquina e, se existir, em `gitlabpr-bundle.pem` na mesma pasta do
|
|
26
|
-
`gitlabpr.properties` (cadeia PEM do servidor). `REQUESTS_CA_BUNDLE` também funciona.
|
|
27
|
-
|
|
28
|
-
## Instalação
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{
|
|
32
|
-
"mcpServers": {
|
|
33
|
-
"gitlabpr": {
|
|
34
|
-
"type": "stdio",
|
|
35
|
-
"command": "uvx",
|
|
36
|
-
"args": ["mcp-gitlabpr@latest"],
|
|
37
|
-
"env": { "UV_SYSTEM_CERTS": "true" }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Desenvolvimento:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
uv sync
|
|
47
|
-
uv run pytest
|
|
48
|
-
uv run mcp dev mcp_gitlabpr/server.py # MCP Inspector
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Tools
|
|
52
|
-
|
|
53
|
-
`projeto` é o caminho `grupo/projeto` (o MCP faz o URL-encode) ou o id numérico. **Sem `projeto`**, ele vem do remote
|
|
54
|
-
do git da pasta em que o MCP roda (`origin`, ou o primeiro remote; aceita `https://`, `ssh://` e `git@host:`), e
|
|
55
|
-
`origem` vem da branch atual. Fora de um repositório git a tool falha pedindo que o agente pergunte ao usuário. O
|
|
56
|
-
cliente MCP precisa iniciar o servidor na pasta do workspace; se não iniciar, informe `projeto`. `iid` é o número do MR no projeto
|
|
57
|
-
(o `!123` da tela). Autor e responsável são usernames: o MCP os converte em id por `GET /users?username=`, porque a 10.8
|
|
58
|
-
só filtra por `author_id`/`assignee_id`.
|
|
59
|
-
|
|
60
|
-
| Tool | Para quê |
|
|
61
|
-
|---|---|
|
|
62
|
-
| `gitlab_listar_mrs(projeto?, todos_projetos=false, estado=opened, busca?, origem?, destino?, autor?, responsavel?, limite≤100, pagina=1)` | MRs do projeto ou, com `todos_projetos`, de todos os visíveis (`scope=all`). Devolve `total`, `proxima_pagina` e os itens resumidos |
|
|
63
|
-
| `gitlab_obter_mr(iid, projeto?, diffs=false)` | Detalhes do MR (descrição, labels, conflitos, merge). Com `diffs`, os arquivos alterados e o diff de cada um, cortado em 4000 caracteres |
|
|
64
|
-
| `gitlab_criar_mr(titulo, destino, origem?, projeto?, descricao?, responsavel?, labels?, remover_origem=false)` | Cria o MR; a branch de origem precisa já estar no GitLab (`git push`). Se já existe um MR aberto entre as mesmas branches, o GitLab responde 409 |
|
|
65
|
-
|
|
66
|
-
`estado` aceita `opened`, `closed`, `merged` ou `all`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|