gitflic-cli-mcp 0.2.0 → 0.3.1
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.
- package/README.md +25 -10
- package/filter.mjs +38 -16
- package/package.json +1 -1
- package/tools.mjs +5 -26
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# gitflic-cli MCP-сервер
|
|
2
2
|
|
|
3
|
-
[Model Context Protocol](https://modelcontextprotocol.io/) сервер, оборачивающий CLI `gitflic` и отдающий **
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io/) сервер, оборачивающий CLI `gitflic` и отдающий **76 типизированных тулзов** поверх команд GitFlic REST для AI-агентов (Claude Code, Cursor и т.п.).
|
|
4
4
|
|
|
5
5
|
## Что внутри
|
|
6
6
|
|
|
@@ -66,22 +66,24 @@ claude mcp add gitflic --env GITFLIC_TOKEN=xxxxxxxx -- npx -y gitflic-cli-mcp
|
|
|
66
66
|
|
|
67
67
|
## Ограничение набора тулзов (экономия контекста)
|
|
68
68
|
|
|
69
|
-
Все
|
|
69
|
+
Все 76 тулзов уходят хосту по `tools/list` ещё до первого вызова — это ~6K токенов «налога» на контекст модели. Если агенту нужны не все, набор можно сузить через **env-переменные** — прописываются в том же `env`-блоке конфига, где `GITFLIC_TOKEN` (своего конфиг-файла у сервера нет, он читает `process.env` при старте).
|
|
70
70
|
|
|
71
71
|
| Переменная | Значение | Что делает |
|
|
72
72
|
|---|---|---|
|
|
73
73
|
| `GITFLIC_MCP_GROUPS` | список групп через запятую/пробел | оставить только тулзы указанных групп |
|
|
74
74
|
| `GITFLIC_MCP_READONLY` | `1` / `true` / `yes` / `on` | оставить только read-only тулзы (без мутаций) |
|
|
75
|
+
| `GITFLIC_MCP_NO_DESTRUCTIVE` | `1` / `true` / `yes` / `on` | убрать деструктивные тулзы (удаление) — см. ниже |
|
|
75
76
|
| `GITFLIC_MCP_TOOLS` | список имён тулзов | явный allowlist (имеет приоритет над `GROUPS`) |
|
|
76
77
|
| `GITFLIC_MCP_EXCLUDE` | список имён тулзов | убрать конкретные тулзы (применяется последним) |
|
|
77
78
|
|
|
78
|
-
Все — опциональные. Без них регистрируются все
|
|
79
|
+
Все — опциональные. Без них регистрируются все 76 (как и раньше). Неизвестные группы/имена не валят сервер — он пишет `WARN` в stderr и игнорирует их.
|
|
79
80
|
|
|
80
81
|
**Порядок применения (precedence):**
|
|
81
82
|
|
|
82
83
|
1. Базовый набор: `GITFLIC_MCP_TOOLS`, если задан; иначе `GITFLIC_MCP_GROUPS`, если задан; иначе — все тулзы.
|
|
83
84
|
2. `GITFLIC_MCP_READONLY` — пересечение с read-only (накладывается поверх **в т.ч. на `GITFLIC_MCP_TOOLS`** — write-тул, явно указанный в allowlist, всё равно будет отсеян; так read-only режим гарантированно не пропускает мутации).
|
|
84
|
-
3. `
|
|
85
|
+
3. `GITFLIC_MCP_NO_DESTRUCTIVE` — вычитает деструктивные (удаление) тулзы (поверх базы/READONLY).
|
|
86
|
+
4. `GITFLIC_MCP_EXCLUDE` — вычитается в самом конце (всегда побеждает).
|
|
85
87
|
|
|
86
88
|
Значения списков — через запятую или пробел; регистр не важен ни для групп, ни для имён тулзов (имена всегда в нижнем регистре).
|
|
87
89
|
|
|
@@ -90,7 +92,7 @@ claude mcp add gitflic --env GITFLIC_TOKEN=xxxxxxxx -- npx -y gitflic-cli-mcp
|
|
|
90
92
|
| Группа | Тулзов | Read / Write |
|
|
91
93
|
|---|---|---|
|
|
92
94
|
| `auth` | 8 | 3 read / 5 write |
|
|
93
|
-
| `blob` |
|
|
95
|
+
| `blob` | 2 | 2 read / 0 write |
|
|
94
96
|
| `branch` | 10 | 5 read / 5 write |
|
|
95
97
|
| `commit` | 4 | 4 read / 0 write |
|
|
96
98
|
| `issue` | 12 | 4 read / 8 write |
|
|
@@ -103,16 +105,29 @@ claude mcp add gitflic --env GITFLIC_TOKEN=xxxxxxxx -- npx -y gitflic-cli-mcp
|
|
|
103
105
|
|
|
104
106
|
> ⚠️ Группа `auth` включает **мутирующие** тулзы (`gitflic_auth_login`, `_logout`, `_switch`, `_bind`, `_unbind`) — они меняют локальный keychain/конфиг, а не сервер, но это всё равно запись. Если нужны только проверки личности — добавь `GITFLIC_MCP_READONLY=1` (останутся `gitflic_auth`, `_list`, `_bindings`) или используй `GITFLIC_MCP_TOOLS` с точным набором.
|
|
105
107
|
|
|
108
|
+
**`GITFLIC_MCP_NO_DESTRUCTIVE=1` — выключить удаление одним флагом.**
|
|
109
|
+
|
|
110
|
+
Жёстко (на уровне регистрации, не через approval-prompt хоста) убирает **только удаляющие** тулзы — модель их вообще не видит. Это надёжнее, чем полагаться на подтверждение в хосте, которое можно прокликать или «разрешить всё агенту».
|
|
111
|
+
|
|
112
|
+
Деструктивный набор (7 — всё, что заканчивается на `_delete`):
|
|
113
|
+
|
|
114
|
+
`gitflic_issue_delete` · `gitflic_issue_comments_delete` · `gitflic_mr_discuss_delete` · `gitflic_branch_delete` · `gitflic_branch_protection_delete` · `gitflic_release_delete` · `gitflic_webhook_delete`
|
|
115
|
+
|
|
116
|
+
Остальные мутации (`*_create`, `*_edit`, `*_close`, `*_cancel`, `gitflic_mr_merge` и т.п.) **остаются** — у них сохраняется `destructiveHint: true`, так что хост по-прежнему спрашивает подтверждение. `gitflic_mr_merge` необратим, но это не удаление — он намеренно вне флага и идёт через approval.
|
|
117
|
+
|
|
106
118
|
**Примеры:**
|
|
107
119
|
|
|
108
120
|
```bash
|
|
109
|
-
# Только MR и issue (~27 тулзов вместо
|
|
121
|
+
# Только MR и issue (~27 тулзов вместо 76)
|
|
110
122
|
claude mcp add gitflic --env GITFLIC_TOKEN=xxxx \
|
|
111
123
|
--env GITFLIC_MCP_GROUPS=mr,issue -- npx -y gitflic-cli-mcp
|
|
112
124
|
|
|
113
125
|
# Read-only агент для код-ревью (только тулзы без мутаций)
|
|
114
126
|
GITFLIC_MCP_READONLY=1 npx -y gitflic-cli-mcp
|
|
115
127
|
|
|
128
|
+
# Может всё, кроме удаления (69 тулзов: создаёт/правит/мёрджит — но не удаляет)
|
|
129
|
+
GITFLIC_MCP_NO_DESTRUCTIVE=1 npx -y gitflic-cli-mcp
|
|
130
|
+
|
|
116
131
|
# Точный набор из четырёх тулзов
|
|
117
132
|
GITFLIC_MCP_TOOLS="gitflic_mr_list,gitflic_mr_view,gitflic_mr_diff,gitflic_mr_create" \
|
|
118
133
|
npx -y gitflic-cli-mcp
|
|
@@ -138,15 +153,15 @@ GITFLIC_MCP_TOOLS="gitflic_mr_list,gitflic_mr_view,gitflic_mr_diff,gitflic_mr_cr
|
|
|
138
153
|
|
|
139
154
|
> Можно поднять один и тот же сервер дважды под разными именами — напр. `gitflic` (полный) и `gitflic-ro` (`GITFLIC_MCP_READONLY=1`) — и держать оба подключёнными. Либо класть конфиг в проектный `.mcp.json` / `.cursor/mcp.json`, чтобы в каждом репозитории грузились свои группы.
|
|
140
155
|
|
|
141
|
-
## Все
|
|
156
|
+
## Все 76 тулзов
|
|
142
157
|
|
|
143
158
|
Полный список по группам. **R** = read-only (`readOnlyHint: true`, не меняет состояние — попадает в `GITFLIC_MCP_READONLY=1`); **W** = write/мутация.
|
|
144
159
|
|
|
145
160
|
### `auth` (8)
|
|
146
161
|
`R` `gitflic_auth` · `W` `gitflic_auth_login` · `W` `gitflic_auth_logout` · `R` `gitflic_auth_list` · `W` `gitflic_auth_switch` · `W` `gitflic_auth_bind` · `W` `gitflic_auth_unbind` · `R` `gitflic_auth_bindings`
|
|
147
162
|
|
|
148
|
-
### `blob` (
|
|
149
|
-
`R` `gitflic_blob_get` · `R` `gitflic_blob_files`
|
|
163
|
+
### `blob` (2)
|
|
164
|
+
`R` `gitflic_blob_get` · `R` `gitflic_blob_files`
|
|
150
165
|
|
|
151
166
|
### `branch` (10)
|
|
152
167
|
`R` `gitflic_branch_list` · `W` `gitflic_branch_create` · `W` `gitflic_branch_delete` · `R` `gitflic_branch_get` · `R` `gitflic_branch_default` · `R` `gitflic_branch_protection_list` · `R` `gitflic_branch_protection_get` · `W` `gitflic_branch_protection_create` · `W` `gitflic_branch_protection_edit` · `W` `gitflic_branch_protection_delete`
|
|
@@ -175,7 +190,7 @@ GITFLIC_MCP_TOOLS="gitflic_mr_list,gitflic_mr_view,gitflic_mr_diff,gitflic_mr_cr
|
|
|
175
190
|
### `webhook` (4)
|
|
176
191
|
`R` `gitflic_webhook_list` · `R` `gitflic_webhook_get` · `W` `gitflic_webhook_create` · `W` `gitflic_webhook_delete`
|
|
177
192
|
|
|
178
|
-
Итого: **
|
|
193
|
+
Итого: **39 read / 37 write**. Каждый тул маппится 1-в-1 на подкоманду CLI (`gitflic …`).
|
|
179
194
|
|
|
180
195
|
## Как это работает
|
|
181
196
|
|
package/filter.mjs
CHANGED
|
@@ -30,6 +30,20 @@ export function isReadOnly(tool) {
|
|
|
30
30
|
return Boolean(tool.annotations && tool.annotations.readOnlyHint === true);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// "Really destructive" = irreversible deletion. This is a deliberately NARROW
|
|
34
|
+
// set (deletions only) used by GITFLIC_MCP_NO_DESTRUCTIVE, which removes these
|
|
35
|
+
// tools at registration — a hard server-side guarantee, stronger than the
|
|
36
|
+
// host's approval prompt (which a user can blanket-allow).
|
|
37
|
+
//
|
|
38
|
+
// Note: this is independent of the MCP `destructiveHint` annotation. Other
|
|
39
|
+
// mutating ops (create/edit/close/cancel and gitflic_mr_merge) keep
|
|
40
|
+
// destructiveHint:true and still go through the host's approval flow — they are
|
|
41
|
+
// NOT removed by this flag. mr_merge is irreversible but is not a deletion, so
|
|
42
|
+
// it is intentionally excluded here.
|
|
43
|
+
export function isDestructive(tool) {
|
|
44
|
+
return /_delete$/.test(tool.name);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// Parse a comma/space-separated env value into a clean array.
|
|
34
48
|
function parseList(value, { lower = false } = {}) {
|
|
35
49
|
if (!value) return [];
|
|
@@ -43,11 +57,18 @@ function parseList(value, { lower = false } = {}) {
|
|
|
43
57
|
const TRUTHY = ["1", "true", "yes", "on"];
|
|
44
58
|
const FALSEY = ["0", "false", "no", "off", ""];
|
|
45
59
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
// Parse a boolean env flag: "1"/"true"/"yes"/"on" → true (case-insensitive);
|
|
61
|
+
// "0"/"false"/"no"/"off"/"" → false. Anything else is treated as false but
|
|
62
|
+
// pushes a warning so a typo (e.g. "enabled") isn't silently ignored.
|
|
63
|
+
function boolFlag(raw, name, warnings) {
|
|
64
|
+
if (raw == null) return false;
|
|
65
|
+
const v = String(raw).trim().toLowerCase();
|
|
66
|
+
if (v !== "" && !TRUTHY.includes(v) && !FALSEY.includes(v)) {
|
|
67
|
+
warnings.push(
|
|
68
|
+
`${name}="${raw}" is not a recognized boolean (use 1/true/yes/on); treating as off.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return TRUTHY.includes(v);
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
// Select which tools to register based on env config.
|
|
@@ -58,7 +79,8 @@ function isTruthy(value) {
|
|
|
58
79
|
// - GITFLIC_MCP_GROUPS (by group), if set; else
|
|
59
80
|
// - all tools.
|
|
60
81
|
// 2. GITFLIC_MCP_READONLY truthy → keep only read-only tools (overlay).
|
|
61
|
-
// 3.
|
|
82
|
+
// 3. GITFLIC_MCP_NO_DESTRUCTIVE truthy → drop deletion tools (overlay).
|
|
83
|
+
// 4. GITFLIC_MCP_EXCLUDE → drop named tools (applied last, always wins).
|
|
62
84
|
//
|
|
63
85
|
// Returns { selected, warnings } — warnings are human-readable strings the
|
|
64
86
|
// caller prints to stderr. Unknown names/groups are warned about, not fatal.
|
|
@@ -74,16 +96,12 @@ export function selectTools(tools, env = {}) {
|
|
|
74
96
|
const wantGroups = parseList(env.GITFLIC_MCP_GROUPS, { lower: true });
|
|
75
97
|
const exclude = parseList(env.GITFLIC_MCP_EXCLUDE, { lower: true });
|
|
76
98
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`GITFLIC_MCP_READONLY="${readonlyRaw}" is not a recognized boolean (use 1/true/yes/on); treating as off.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
99
|
+
const readonly = boolFlag(env.GITFLIC_MCP_READONLY, "GITFLIC_MCP_READONLY", warnings);
|
|
100
|
+
const noDestructive = boolFlag(
|
|
101
|
+
env.GITFLIC_MCP_NO_DESTRUCTIVE,
|
|
102
|
+
"GITFLIC_MCP_NO_DESTRUCTIVE",
|
|
103
|
+
warnings,
|
|
104
|
+
);
|
|
87
105
|
|
|
88
106
|
let base;
|
|
89
107
|
if (wantTools.length) {
|
|
@@ -121,6 +139,10 @@ export function selectTools(tools, env = {}) {
|
|
|
121
139
|
}
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
if (noDestructive) {
|
|
143
|
+
base = base.filter((t) => !isDestructive(t));
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
if (exclude.length) {
|
|
125
147
|
for (const n of exclude) {
|
|
126
148
|
if (!byName.has(n)) {
|
package/package.json
CHANGED
package/tools.mjs
CHANGED
|
@@ -242,10 +242,10 @@ export const TOOLS = [
|
|
|
242
242
|
{
|
|
243
243
|
name: "gitflic_mr_create",
|
|
244
244
|
description:
|
|
245
|
-
"Create a new merge request. If
|
|
245
|
+
"Create a new merge request. `description` is required (GitFlic rejects empty MR descriptions). If `from` is omitted, uses current git branch; if `to` is omitted, uses project default branch.",
|
|
246
246
|
inputSchema: {
|
|
247
247
|
type: "object",
|
|
248
|
-
required: ["project", "title"],
|
|
248
|
+
required: ["project", "title", "description"],
|
|
249
249
|
properties: {
|
|
250
250
|
project: { type: "string" },
|
|
251
251
|
title: { type: "string" },
|
|
@@ -1430,7 +1430,7 @@ export const TOOLS = [
|
|
|
1430
1430
|
},
|
|
1431
1431
|
{
|
|
1432
1432
|
name: "gitflic_project_edit",
|
|
1433
|
-
description: "Edit project
|
|
1433
|
+
description: "Edit project title, description, or language (via change-setting). Visibility and default branch are not editable over the REST API.",
|
|
1434
1434
|
inputSchema: {
|
|
1435
1435
|
type: "object",
|
|
1436
1436
|
required: ["project"],
|
|
@@ -1438,18 +1438,14 @@ export const TOOLS = [
|
|
|
1438
1438
|
project: { type: "string" },
|
|
1439
1439
|
title: { type: "string" },
|
|
1440
1440
|
description: { type: "string" },
|
|
1441
|
-
isPrivate: { type: "boolean" },
|
|
1442
1441
|
language: { type: "string" },
|
|
1443
|
-
defaultBranch: { type: "string" },
|
|
1444
1442
|
},
|
|
1445
1443
|
},
|
|
1446
1444
|
handler: (a) => {
|
|
1447
1445
|
const args = ["project", "edit", a.project, "--format", "json"];
|
|
1448
1446
|
if (a.title) args.push("--title", a.title);
|
|
1449
1447
|
if (a.description !== undefined) args.push("--description", a.description);
|
|
1450
|
-
if (a.isPrivate !== undefined) args.push(a.isPrivate ? "--private" : "--public");
|
|
1451
1448
|
if (a.language) args.push("--language", a.language);
|
|
1452
|
-
if (a.defaultBranch) args.push("--default-branch", a.defaultBranch);
|
|
1453
1449
|
return args;
|
|
1454
1450
|
},
|
|
1455
1451
|
annotations: {
|
|
@@ -1540,8 +1536,8 @@ export const TOOLS = [
|
|
|
1540
1536
|
},
|
|
1541
1537
|
},
|
|
1542
1538
|
handler: (a) => {
|
|
1543
|
-
const args = ["blob", "
|
|
1544
|
-
if (a.path) args.push("--
|
|
1539
|
+
const args = ["blob", "recursive", "--project", a.project, "--format", "json"];
|
|
1540
|
+
if (a.path) args.push("--directory", a.path);
|
|
1545
1541
|
if (a.branch) args.push("--branch", a.branch);
|
|
1546
1542
|
return args;
|
|
1547
1543
|
},
|
|
@@ -1553,23 +1549,6 @@ export const TOOLS = [
|
|
|
1553
1549
|
openWorldHint: true,
|
|
1554
1550
|
},
|
|
1555
1551
|
},
|
|
1556
|
-
{
|
|
1557
|
-
name: "gitflic_blob_archive",
|
|
1558
|
-
description: "Download a repo archive (tarball). Returns JSON metadata; use shell to stream the body.",
|
|
1559
|
-
inputSchema: {
|
|
1560
|
-
type: "object",
|
|
1561
|
-
required: ["project", "branch"],
|
|
1562
|
-
properties: { project: { type: "string" }, branch: { type: "string" } },
|
|
1563
|
-
},
|
|
1564
|
-
handler: (a) => ["blob", "archive", a.branch, "--project", a.project, "--format", "json"],
|
|
1565
|
-
annotations: {
|
|
1566
|
-
title: "Blob Archive",
|
|
1567
|
-
readOnlyHint: true,
|
|
1568
|
-
destructiveHint: false,
|
|
1569
|
-
idempotentHint: false,
|
|
1570
|
-
openWorldHint: true,
|
|
1571
|
-
},
|
|
1572
|
-
},
|
|
1573
1552
|
|
|
1574
1553
|
// ===== User: get / projects / followers =================================
|
|
1575
1554
|
{
|