mcp-gitlabpr 1.0.2__tar.gz → 2.0.0__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.2 → mcp_gitlabpr-2.0.0}/PKG-INFO +20 -20
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/README.md +19 -19
- mcp_gitlabpr-2.0.0/mcp_gitlabpr/config.py +34 -0
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/mcp_gitlabpr/server.py +8 -13
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/pyproject.toml +1 -1
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/tests/test_server.py +13 -9
- mcp_gitlabpr-1.0.2/mcp_gitlabpr/config.py +0 -42
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/.gitignore +0 -0
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/LICENSE +0 -0
- {mcp_gitlabpr-1.0.2 → mcp_gitlabpr-2.0.0}/mcp_gitlabpr/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-gitlabpr
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
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
5
|
Project-URL: Repository, https://github.com/dxaraujo/mcp-gitlabpr
|
|
6
6
|
Author-email: Daniel Xavier Araújo <danielxaraujo@gmail.com>
|
|
@@ -18,25 +18,15 @@ v4 com personal access token e só os parâmetros que já existem na **GitLab CE
|
|
|
18
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
19
|
funciona em versões mais novas.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Configuração
|
|
22
22
|
|
|
23
|
-
|
|
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`.
|
|
23
|
+
Só por variáveis de ambiente, no `env` do servidor MCP:
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
| Variável | Obrigatória | O que é |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `GITLAB_API_URL` | sim | URL do GitLab (com ou sem `/api/v4`) |
|
|
28
|
+
| `GITLAB_API_TOKEN` | sim | Personal access token com o escopo `api`, enviado no header `PRIVATE-TOKEN` (sai de *Settings → Access Tokens*) |
|
|
29
|
+
| `GITLAB_CA_BUNDLE` | não | Caminho de um arquivo PEM com a cadeia do servidor, somada às CAs da máquina |
|
|
40
30
|
|
|
41
31
|
## Instalação
|
|
42
32
|
|
|
@@ -47,12 +37,22 @@ O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `
|
|
|
47
37
|
"type": "stdio",
|
|
48
38
|
"command": "uvx",
|
|
49
39
|
"args": ["mcp-gitlabpr@latest"],
|
|
50
|
-
"env": {
|
|
40
|
+
"env": {
|
|
41
|
+
"GITLAB_API_URL": "https://gitlab.SEU-SERVIDOR",
|
|
42
|
+
"GITLAB_API_TOKEN": "${GITLAB_API_TOKEN}",
|
|
43
|
+
"UV_SYSTEM_CERTS": "true"
|
|
44
|
+
}
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
47
|
}
|
|
54
48
|
```
|
|
55
49
|
|
|
50
|
+
**Como passar as variáveis:** defina-as no bloco `env` do servidor MCP, como no exemplo. Para não gravar segredos em
|
|
51
|
+
texto puro, use referências: `${VAR}` no Kiro IDE, `${env:VAR}` no Kiro CLI e `{env:VAR}` no opencode, e deixe o valor
|
|
52
|
+
no gerenciador de segredos ou no ambiente de onde a ferramenta é aberta. Não há arquivo de configuração nem caminho padrão.
|
|
53
|
+
No macOS, um app aberto pelo Dock não herda o `export` do shell: abra a ferramenta pelo terminal ou defina a variável no
|
|
54
|
+
ambiente da sessão. Evite `export` no perfil do shell quando o agente puder rodar comandos, porque ele conseguiria ler o valor.
|
|
55
|
+
|
|
56
56
|
Desenvolvimento:
|
|
57
57
|
|
|
58
58
|
```bash
|
|
@@ -69,7 +69,7 @@ informada, o caminho absoluto da raiz do workspace, e `origem` vem da branch atu
|
|
|
69
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
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
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
|
|
72
|
+
remote ou da URL precisa ser o de `GITLAB_API_URL`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
|
|
73
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
74
|
filtra por `author_id`/`assignee_id`.
|
|
75
75
|
|
|
@@ -5,25 +5,15 @@ v4 com personal access token e só os parâmetros que já existem na **GitLab CE
|
|
|
5
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
6
|
funciona em versões mais novas.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Configuração
|
|
9
9
|
|
|
10
|
-
|
|
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`.
|
|
10
|
+
Só por variáveis de ambiente, no `env` do servidor MCP:
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
| Variável | Obrigatória | O que é |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `GITLAB_API_URL` | sim | URL do GitLab (com ou sem `/api/v4`) |
|
|
15
|
+
| `GITLAB_API_TOKEN` | sim | Personal access token com o escopo `api`, enviado no header `PRIVATE-TOKEN` (sai de *Settings → Access Tokens*) |
|
|
16
|
+
| `GITLAB_CA_BUNDLE` | não | Caminho de um arquivo PEM com a cadeia do servidor, somada às CAs da máquina |
|
|
27
17
|
|
|
28
18
|
## Instalação
|
|
29
19
|
|
|
@@ -34,12 +24,22 @@ O token sai de *Settings → Access Tokens* com o escopo `api` e vai no header `
|
|
|
34
24
|
"type": "stdio",
|
|
35
25
|
"command": "uvx",
|
|
36
26
|
"args": ["mcp-gitlabpr@latest"],
|
|
37
|
-
"env": {
|
|
27
|
+
"env": {
|
|
28
|
+
"GITLAB_API_URL": "https://gitlab.SEU-SERVIDOR",
|
|
29
|
+
"GITLAB_API_TOKEN": "${GITLAB_API_TOKEN}",
|
|
30
|
+
"UV_SYSTEM_CERTS": "true"
|
|
31
|
+
}
|
|
38
32
|
}
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
```
|
|
42
36
|
|
|
37
|
+
**Como passar as variáveis:** defina-as no bloco `env` do servidor MCP, como no exemplo. Para não gravar segredos em
|
|
38
|
+
texto puro, use referências: `${VAR}` no Kiro IDE, `${env:VAR}` no Kiro CLI e `{env:VAR}` no opencode, e deixe o valor
|
|
39
|
+
no gerenciador de segredos ou no ambiente de onde a ferramenta é aberta. Não há arquivo de configuração nem caminho padrão.
|
|
40
|
+
No macOS, um app aberto pelo Dock não herda o `export` do shell: abra a ferramenta pelo terminal ou defina a variável no
|
|
41
|
+
ambiente da sessão. Evite `export` no perfil do shell quando o agente puder rodar comandos, porque ele conseguiria ler o valor.
|
|
42
|
+
|
|
43
43
|
Desenvolvimento:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
@@ -56,7 +56,7 @@ informada, o caminho absoluto da raiz do workspace, e `origem` vem da branch atu
|
|
|
56
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
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
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
|
|
59
|
+
remote ou da URL precisa ser o de `GITLAB_API_URL`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
|
|
60
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
61
|
filtra por `author_id`/`assignee_id`.
|
|
62
62
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Carrega URL, token e certificado do GitLab das variáveis de ambiente."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
REQUIRED = ("GITLAB_API_URL", "GITLAB_API_TOKEN")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class GitlabConfig:
|
|
13
|
+
url: str
|
|
14
|
+
token: str = field(repr=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ca_bundle() -> Path | None:
|
|
18
|
+
"""Cadeia PEM extra apontada por GITLAB_CA_BUNDLE, somada às CAs da máquina."""
|
|
19
|
+
valor = os.environ.get("GITLAB_CA_BUNDLE", "").strip()
|
|
20
|
+
if not valor:
|
|
21
|
+
return None
|
|
22
|
+
path = Path(valor).expanduser()
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
raise ValueError(f"GITLAB_CA_BUNDLE aponta para um arquivo que não existe: {path}")
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_config() -> GitlabConfig:
|
|
29
|
+
missing = [k for k in REQUIRED if not os.environ.get(k, "").strip()]
|
|
30
|
+
if missing:
|
|
31
|
+
raise ValueError(f"Variável(is) de ambiente ausente(s): {', '.join(missing)}")
|
|
32
|
+
ca_bundle() # arquivo errado aparece já na carga
|
|
33
|
+
url = os.environ["GITLAB_API_URL"].strip().rstrip("/").removesuffix("/api/v4")
|
|
34
|
+
return GitlabConfig(url=url, token=os.environ["GITLAB_API_TOKEN"].strip())
|
|
@@ -5,7 +5,7 @@ GitLab CE 10.8: autor e responsável vão por id (`author_username` é de versõ
|
|
|
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
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
|
|
8
|
+
precisa ser o de GITLAB_API_URL.
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ from requests.adapters import HTTPAdapter
|
|
|
21
21
|
from mcp.server.mcpserver import MCPServer
|
|
22
22
|
from mcp.server.mcpserver.exceptions import ToolError
|
|
23
23
|
|
|
24
|
-
from .config import GitlabConfig,
|
|
24
|
+
from .config import GitlabConfig, ca_bundle, load_config
|
|
25
25
|
|
|
26
26
|
mcp = MCPServer(
|
|
27
27
|
"gitlabpr",
|
|
@@ -58,18 +58,13 @@ def tool(fn):
|
|
|
58
58
|
|
|
59
59
|
# ---------------------------------------------------------------- HTTP
|
|
60
60
|
|
|
61
|
-
def ca_bundle() -> Path:
|
|
62
|
-
"""CAs extras (ex.: cadeia interna do GitLab), ao lado do gitlabpr.properties."""
|
|
63
|
-
return default_path().parent / "gitlabpr-bundle.pem"
|
|
64
|
-
|
|
65
|
-
|
|
66
61
|
class _TlsAdapter(HTTPAdapter):
|
|
67
|
-
"""CAs do sistema +
|
|
62
|
+
"""CAs do sistema + a cadeia de GITLAB_CA_BUNDLE, se definida."""
|
|
68
63
|
|
|
69
64
|
def init_poolmanager(self, *args, **kwargs):
|
|
70
65
|
ctx = ssl.create_default_context()
|
|
71
|
-
if ca_bundle()
|
|
72
|
-
ctx.load_verify_locations(
|
|
66
|
+
if ca := ca_bundle():
|
|
67
|
+
ctx.load_verify_locations(ca)
|
|
73
68
|
super().init_poolmanager(*args, ssl_context=ctx, **kwargs)
|
|
74
69
|
|
|
75
70
|
|
|
@@ -87,8 +82,8 @@ class GitlabSession:
|
|
|
87
82
|
resp = self.session.request(method, url, timeout=TIMEOUT, **kwargs)
|
|
88
83
|
except requests.exceptions.SSLError as exc:
|
|
89
84
|
raise RuntimeError(
|
|
90
|
-
f"Certificado do servidor não confiável em {url}.
|
|
91
|
-
f"
|
|
85
|
+
f"Certificado do servidor não confiável em {url}. Instale a CA na máquina ou aponte GITLAB_CA_BUNDLE "
|
|
86
|
+
f"para a cadeia de certificados (PEM) e reinicie o MCP. Detalhe: {exc}") from exc
|
|
92
87
|
if resp.status_code == 401:
|
|
93
88
|
raise RuntimeError(f"HTTP 401 em {method} {url}: token inválido ou expirado (precisa do escopo `api`).")
|
|
94
89
|
if not resp.ok:
|
|
@@ -101,7 +96,7 @@ _lock = threading.Lock()
|
|
|
101
96
|
|
|
102
97
|
|
|
103
98
|
def get_session() -> GitlabSession:
|
|
104
|
-
"""Sessão única por processo; carrega
|
|
99
|
+
"""Sessão única por processo; carrega as variáveis de ambiente na primeira chamada."""
|
|
105
100
|
global _instance
|
|
106
101
|
with _lock:
|
|
107
102
|
if _instance is None:
|
|
@@ -54,15 +54,19 @@ def fake(tmp_path_factory, monkeypatch):
|
|
|
54
54
|
server.set_session(None)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def test_config(tmp_path):
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
def test_config(monkeypatch, tmp_path):
|
|
58
|
+
for k in ("GITLAB_API_URL", "GITLAB_API_TOKEN", "GITLAB_CA_BUNDLE"):
|
|
59
|
+
monkeypatch.delenv(k, raising=False)
|
|
60
|
+
monkeypatch.setenv("GITLAB_API_URL", "https://gitlab.test/api/v4/")
|
|
61
|
+
with pytest.raises(ValueError, match="GITLAB_API_TOKEN"):
|
|
62
|
+
config.load_config()
|
|
63
|
+
monkeypatch.setenv("GITLAB_API_TOKEN", "a%b")
|
|
64
|
+
assert config.load_config() == GitlabConfig(url="https://gitlab.test", token="a%b")
|
|
65
|
+
monkeypatch.setenv("GITLAB_CA_BUNDLE", str(tmp_path / "x.pem"))
|
|
66
|
+
with pytest.raises(ValueError, match="GITLAB_CA_BUNDLE"):
|
|
67
|
+
config.load_config()
|
|
68
|
+
(tmp_path / "x.pem").write_text("pem")
|
|
69
|
+
assert config.ca_bundle() == tmp_path / "x.pem"
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def test_listar_projeto(fake):
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Carrega URL e token do GitLab a partir de gitlabpr.properties."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import configparser
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
REQUIRED = ("url", "token")
|
|
11
|
-
TEMPLATE = "[DEFAULT]\nurl = https://gitlab.SEU-SERVIDOR\ntoken = SEU_PERSONAL_ACCESS_TOKEN"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class GitlabConfig:
|
|
16
|
-
url: str
|
|
17
|
-
token: str = field(repr=False)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def default_path() -> Path:
|
|
21
|
-
if env := os.environ.get("MCP_GITLABPR_CONFIG"):
|
|
22
|
-
return Path(env)
|
|
23
|
-
if sys.platform == "win32":
|
|
24
|
-
return Path(os.environ["APPDATA"]) / "mcp-gitlabpr" / "gitlabpr.properties"
|
|
25
|
-
return Path.home() / ".config" / "mcp-gitlabpr" / "gitlabpr.properties"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def load_config(path: Path | None = None) -> GitlabConfig:
|
|
29
|
-
path = path or default_path()
|
|
30
|
-
if not path.is_file():
|
|
31
|
-
raise FileNotFoundError(f"Arquivo de credenciais não encontrado: {path}\nCrie-o com:\n{TEMPLATE}")
|
|
32
|
-
# interpolation=None: tokens podem conter '%'
|
|
33
|
-
parser = configparser.ConfigParser(interpolation=None)
|
|
34
|
-
try:
|
|
35
|
-
parser.read(path, encoding="utf-8")
|
|
36
|
-
except configparser.Error as exc:
|
|
37
|
-
raise ValueError(f"{path} inválido: {exc}") from exc
|
|
38
|
-
section = parser.defaults()
|
|
39
|
-
missing = [k for k in REQUIRED if not section.get(k, "").strip()]
|
|
40
|
-
if missing:
|
|
41
|
-
raise ValueError(f"Chave(s) ausente(s) em {path} [DEFAULT]: {', '.join(missing)}")
|
|
42
|
-
return GitlabConfig(url=section["url"].strip().rstrip("/"), token=section["token"].strip())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|