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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gitlabpr
3
- Version: 1.0.2
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
- ## Credenciais
21
+ ## Configuração
22
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`.
23
+ Só por variáveis de ambiente, no `env` do servidor MCP:
37
24
 
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.
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": { "UV_SYSTEM_CERTS": "true" }
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 do `gitlabpr.properties`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
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
- ## Credenciais
8
+ ## Configuração
9
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`.
10
+ Só por variáveis de ambiente, no `env` do servidor MCP:
24
11
 
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.
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": { "UV_SYSTEM_CERTS": "true" }
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 do `gitlabpr.properties`; se não for, a tool recusa. `iid` é o número do MR no projeto (o `!123`
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 do gitlabpr.properties.
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, default_path, load_config
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 + gitlabpr-bundle.pem, se existir."""
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().is_file():
72
- ctx.load_verify_locations(ca_bundle())
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}. Grave a cadeia de certificados (PEM) em "
91
- f"{ca_bundle()} (ou aponte REQUESTS_CA_BUNDLE para ela) e reinicie o MCP. Detalhe: {exc}") from exc
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 o gitlabpr.properties na primeira chamada."""
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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-gitlabpr"
3
- version = "1.0.2"
3
+ version = "2.0.0"
4
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"
@@ -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
- with pytest.raises(FileNotFoundError):
59
- config.load_config(tmp_path / "x")
60
- p = tmp_path / "gitlabpr.properties"
61
- p.write_text("[DEFAULT]\nurl = https://gitlab.test/\n")
62
- with pytest.raises(ValueError, match="token"):
63
- config.load_config(p)
64
- p.write_text("[DEFAULT]\nurl = https://gitlab.test/\ntoken = a%b\n")
65
- assert config.load_config(p) == GitlabConfig(url="https://gitlab.test", token="a%b")
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