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.
Files changed (4) hide show
  1. package/README.md +25 -10
  2. package/filter.mjs +38 -16
  3. package/package.json +1 -1
  4. 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` и отдающий **77 типизированных тулзов** поверх команд GitFlic REST для AI-агентов (Claude Code, Cursor и т.п.).
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
- Все 77 тулзов уходят хосту по `tools/list` ещё до первого вызова — это ~6K токенов «налога» на контекст модели. Если агенту нужны не все, набор можно сузить через **env-переменные** — прописываются в том же `env`-блоке конфига, где `GITFLIC_TOKEN` (своего конфиг-файла у сервера нет, он читает `process.env` при старте).
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
- Все — опциональные. Без них регистрируются все 77 (как и раньше). Неизвестные группы/имена не валят сервер — он пишет `WARN` в stderr и игнорирует их.
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. `GITFLIC_MCP_EXCLUDE` — вычитается в самом конце (всегда побеждает).
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` | 3 | 3 read / 0 write |
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 тулзов вместо 77)
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
- ## Все 77 тулзов
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` (3)
149
- `R` `gitflic_blob_get` · `R` `gitflic_blob_files` · `R` `gitflic_blob_archive`
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
- Итого: **40 read / 37 write**. Каждый тул маппится 1-в-1 на подкоманду CLI (`gitflic …`).
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
- // Truthy env flag: "1", "true", "yes", "on" (case-insensitive). Anything
47
- // else (including "0", "false", "", undefined) is falsey.
48
- function isTruthy(value) {
49
- if (value == null) return false;
50
- return TRUTHY.includes(String(value).trim().toLowerCase());
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. GITFLIC_MCP_EXCLUDE → drop named tools (applied last, always wins).
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 readonlyRaw = env.GITFLIC_MCP_READONLY;
78
- const readonly = isTruthy(readonlyRaw);
79
- if (readonlyRaw != null) {
80
- const v = String(readonlyRaw).trim().toLowerCase();
81
- if (v !== "" && !TRUTHY.includes(v) && !FALSEY.includes(v)) {
82
- warnings.push(
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitflic-cli-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "MCP server wrapping the gitflic CLI — exposes the GitFlic REST API as typed tools for AI agents (Claude Code, Cursor, etc.)",
5
5
  "type": "module",
6
6
  "bin": {
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 --from is omitted, uses current git branch. If --to is omitted, uses project default branch.",
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 metadata (title, description, default branch, visibility).",
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", "files", "--project", a.project, "--format", "json"];
1544
- if (a.path) args.push("--path", a.path);
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
  {