gitflic-cli-mcp 0.1.0 → 0.2.0

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 CHANGED
@@ -64,30 +64,118 @@ claude mcp add gitflic --env GITFLIC_TOKEN=xxxxxxxx -- npx -y gitflic-cli-mcp
64
64
 
65
65
  Та же структура. Перезапустите хост. (Из исходников: `"command": "node", "args": ["/absolute/path/to/gitflic-cli/mcp-server/server.mjs"]`.)
66
66
 
67
- ## Тулзы (77) — основные
68
-
69
- Нижевыборка; полный список из 77 тулзов хост получает по `tools/list`.
70
-
71
- | Тулз | Маппится на |
72
- |---|---|
73
- | `gitflic_auth` | `gitflic auth` |
74
- | `gitflic_project_resolve` | `gitflic project get` |
75
- | `gitflic_mr_list` / `view` / `diff` | `gitflic mr list/view/diff` |
76
- | `gitflic_mr_create` | `gitflic mr create` (авто from→to) |
77
- | `gitflic_mr_approve` / `merge` / `close` | `gitflic mr approve/merge/close` |
78
- | `gitflic_mr_comment` (общий + инлайн-якорный) | `gitflic mr comment` |
79
- | `gitflic_mr_discuss_list` / `reply` / `resolve` | `gitflic mr discuss …` |
80
- | `gitflic_issue_list` / `view` / `create` / `close` / `comment` | `gitflic issue …` |
81
- | `gitflic_branch_list` / `create` / `delete` | `gitflic branch …` |
82
- | `gitflic_tag_list` / `create` | `gitflic tag …` |
83
- | `gitflic_release_list` / `create` | `gitflic release …` |
84
- | `gitflic_commit_list` | `gitflic commit list` |
85
- | `gitflic_user_me` / `gitflic_user_search` | `gitflic user …` |
86
- | `gitflic_project_list_my` | `gitflic project list --scope my` |
87
- | `gitflic_webhook_list` | `gitflic webhook list` |
88
- | `gitflic_branch_protection_list` | `gitflic branch protection list` |
89
-
90
- Эта таблица лишь самые ходовые тулзы. Всего их 77 (каждая команда CLI имеет свой тул); что не попало в таблицу — всё равно доступно как MCP-тул или через `gitflic …` напрямую.
67
+ ## Ограничение набора тулзов (экономия контекста)
68
+
69
+ Все 77 тулзов уходят хосту по `tools/list` ещё до первого вызова это ~6K токенов «налога» на контекст модели. Если агенту нужны не все, набор можно сузить через **env-переменные** — прописываются в том же `env`-блоке конфига, где `GITFLIC_TOKEN` (своего конфиг-файла у сервера нет, он читает `process.env` при старте).
70
+
71
+ | Переменная | Значение | Что делает |
72
+ |---|---|---|
73
+ | `GITFLIC_MCP_GROUPS` | список групп через запятую/пробел | оставить только тулзы указанных групп |
74
+ | `GITFLIC_MCP_READONLY` | `1` / `true` / `yes` / `on` | оставить только read-only тулзы (без мутаций) |
75
+ | `GITFLIC_MCP_TOOLS` | список имён тулзов | явный allowlist (имеет приоритет над `GROUPS`) |
76
+ | `GITFLIC_MCP_EXCLUDE` | список имён тулзов | убрать конкретные тулзы (применяется последним) |
77
+
78
+ Все опциональные. Без них регистрируются все 77 (как и раньше). Неизвестные группы/имена не валят сервер — он пишет `WARN` в stderr и игнорирует их.
79
+
80
+ **Порядок применения (precedence):**
81
+
82
+ 1. Базовый набор: `GITFLIC_MCP_TOOLS`, если задан; иначе `GITFLIC_MCP_GROUPS`, если задан; иначе — все тулзы.
83
+ 2. `GITFLIC_MCP_READONLY` пересечение с read-only (накладывается поверх **в т.ч. на `GITFLIC_MCP_TOOLS`** write-тул, явно указанный в allowlist, всё равно будет отсеян; так read-only режим гарантированно не пропускает мутации).
84
+ 3. `GITFLIC_MCP_EXCLUDE` вычитается в самом конце (всегда побеждает).
85
+
86
+ Значения списков через запятую или пробел; регистр не важен ни для групп, ни для имён тулзов (имена всегда в нижнем регистре).
87
+
88
+ **Допустимые значения `GITFLIC_MCP_GROUPS`** (группа = первый сегмент имени после `gitflic_`):
89
+
90
+ | Группа | Тулзов | Read / Write |
91
+ |---|---|---|
92
+ | `auth` | 8 | 3 read / 5 write |
93
+ | `blob` | 3 | 3 read / 0 write |
94
+ | `branch` | 10 | 5 read / 5 write |
95
+ | `commit` | 4 | 4 read / 0 write |
96
+ | `issue` | 12 | 4 read / 8 write |
97
+ | `mr` | 15 | 5 read / 10 write |
98
+ | `project` | 7 | 4 read / 3 write |
99
+ | `release` | 6 | 3 read / 3 write |
100
+ | `tag` | 3 | 2 read / 1 write |
101
+ | `user` | 5 | 5 read / 0 write |
102
+ | `webhook` | 4 | 2 read / 2 write |
103
+
104
+ > ⚠️ Группа `auth` включает **мутирующие** тулзы (`gitflic_auth_login`, `_logout`, `_switch`, `_bind`, `_unbind`) — они меняют локальный keychain/конфиг, а не сервер, но это всё равно запись. Если нужны только проверки личности — добавь `GITFLIC_MCP_READONLY=1` (останутся `gitflic_auth`, `_list`, `_bindings`) или используй `GITFLIC_MCP_TOOLS` с точным набором.
105
+
106
+ **Примеры:**
107
+
108
+ ```bash
109
+ # Только MR и issue (~27 тулзов вместо 77)
110
+ claude mcp add gitflic --env GITFLIC_TOKEN=xxxx \
111
+ --env GITFLIC_MCP_GROUPS=mr,issue -- npx -y gitflic-cli-mcp
112
+
113
+ # Read-only агент для код-ревью (только тулзы без мутаций)
114
+ GITFLIC_MCP_READONLY=1 npx -y gitflic-cli-mcp
115
+
116
+ # Точный набор из четырёх тулзов
117
+ GITFLIC_MCP_TOOLS="gitflic_mr_list,gitflic_mr_view,gitflic_mr_diff,gitflic_mr_create" \
118
+ npx -y gitflic-cli-mcp
119
+ ```
120
+
121
+ В JSON-конфиге это тот же `env`-блок:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "gitflic": {
127
+ "command": "npx",
128
+ "args": ["-y", "gitflic-cli-mcp"],
129
+ "env": {
130
+ "GITFLIC_TOKEN": "xxxx",
131
+ "GITFLIC_MCP_GROUPS": "mr,issue",
132
+ "GITFLIC_MCP_READONLY": "1"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ > Можно поднять один и тот же сервер дважды под разными именами — напр. `gitflic` (полный) и `gitflic-ro` (`GITFLIC_MCP_READONLY=1`) — и держать оба подключёнными. Либо класть конфиг в проектный `.mcp.json` / `.cursor/mcp.json`, чтобы в каждом репозитории грузились свои группы.
140
+
141
+ ## Все 77 тулзов
142
+
143
+ Полный список по группам. **R** = read-only (`readOnlyHint: true`, не меняет состояние — попадает в `GITFLIC_MCP_READONLY=1`); **W** = write/мутация.
144
+
145
+ ### `auth` (8)
146
+ `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
+
148
+ ### `blob` (3)
149
+ `R` `gitflic_blob_get` · `R` `gitflic_blob_files` · `R` `gitflic_blob_archive`
150
+
151
+ ### `branch` (10)
152
+ `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`
153
+
154
+ ### `commit` (4)
155
+ `R` `gitflic_commit_list` · `R` `gitflic_commit_get` · `R` `gitflic_commit_files` · `R` `gitflic_commit_compare`
156
+
157
+ ### `issue` (12)
158
+ `R` `gitflic_issue_list` · `R` `gitflic_issue_view` · `W` `gitflic_issue_create` · `W` `gitflic_issue_close` · `W` `gitflic_issue_comment` · `W` `gitflic_issue_edit` · `W` `gitflic_issue_reopen` · `W` `gitflic_issue_delete` · `R` `gitflic_issue_comments_list` · `W` `gitflic_issue_comments_edit` · `W` `gitflic_issue_comments_delete` · `R` `gitflic_issue_relations_list`
159
+
160
+ ### `mr` (15)
161
+ `R` `gitflic_mr_list` · `R` `gitflic_mr_view` · `R` `gitflic_mr_diff` · `R` `gitflic_mr_by_commit` · `R` `gitflic_mr_discuss_list` · `W` `gitflic_mr_create` · `W` `gitflic_mr_approve` · `W` `gitflic_mr_merge` · `W` `gitflic_mr_close` · `W` `gitflic_mr_cancel` · `W` `gitflic_mr_edit` · `W` `gitflic_mr_comment` · `W` `gitflic_mr_discuss_reply` · `W` `gitflic_mr_discuss_resolve` · `W` `gitflic_mr_discuss_delete`
162
+
163
+ ### `project` (7)
164
+ `R` `gitflic_project_resolve` · `R` `gitflic_project_get` · `R` `gitflic_project_list_my` · `R` `gitflic_project_member_list` · `W` `gitflic_project_create` · `W` `gitflic_project_edit` · `W` `gitflic_project_member_invite`
165
+
166
+ ### `release` (6)
167
+ `R` `gitflic_release_list` · `R` `gitflic_release_get` · `R` `gitflic_release_latest` · `W` `gitflic_release_create` · `W` `gitflic_release_edit` · `W` `gitflic_release_delete`
168
+
169
+ ### `tag` (3)
170
+ `R` `gitflic_tag_list` · `R` `gitflic_tag_get` · `W` `gitflic_tag_create`
171
+
172
+ ### `user` (5)
173
+ `R` `gitflic_user_me` · `R` `gitflic_user_search` · `R` `gitflic_user_get` · `R` `gitflic_user_projects` · `R` `gitflic_user_followers`
174
+
175
+ ### `webhook` (4)
176
+ `R` `gitflic_webhook_list` · `R` `gitflic_webhook_get` · `W` `gitflic_webhook_create` · `W` `gitflic_webhook_delete`
177
+
178
+ Итого: **40 read / 37 write**. Каждый тул маппится 1-в-1 на подкоманду CLI (`gitflic …`).
91
179
 
92
180
  ## Как это работает
93
181
 
package/filter.mjs ADDED
@@ -0,0 +1,140 @@
1
+ // Tool selection for the MCP server.
2
+ //
3
+ // Lets users scope WHICH of the tools get registered, to keep the model's
4
+ // context small (the full catalog costs ~6K tokens before the first call).
5
+ // All config is via environment variables — see README "Ограничение набора
6
+ // тулзов (экономия контекста)".
7
+ //
8
+ // Env vars (all optional; default = register everything):
9
+ // GITFLIC_MCP_GROUPS comma/space list of groups, e.g. "mr,issue"
10
+ // GITFLIC_MCP_READONLY "1"/"true"/"yes"/"on" → only read-only tools
11
+ // GITFLIC_MCP_TOOLS comma/space list of exact tool names (allowlist)
12
+ // GITFLIC_MCP_EXCLUDE comma/space list of exact tool names to drop
13
+ //
14
+ // Pure + testable: selectTools() takes an explicit env object, so tests never
15
+ // touch process.env.
16
+
17
+ // Derive the group of a tool from its name: gitflic_<group>_<action>.
18
+ // e.g. gitflic_mr_create → "mr", gitflic_branch_protection_list → "branch".
19
+ export function toolGroup(name) {
20
+ return name.replace(/^gitflic_/, "").split("_")[0];
21
+ }
22
+
23
+ // All distinct groups present in the catalog, sorted.
24
+ export function groupsOf(tools) {
25
+ return [...new Set(tools.map((t) => toolGroup(t.name)))].sort();
26
+ }
27
+
28
+ // A tool is read-only iff its annotations say so.
29
+ export function isReadOnly(tool) {
30
+ return Boolean(tool.annotations && tool.annotations.readOnlyHint === true);
31
+ }
32
+
33
+ // Parse a comma/space-separated env value into a clean array.
34
+ function parseList(value, { lower = false } = {}) {
35
+ if (!value) return [];
36
+ return value
37
+ .split(/[,\s]+/)
38
+ .map((s) => s.trim())
39
+ .filter(Boolean)
40
+ .map((s) => (lower ? s.toLowerCase() : s));
41
+ }
42
+
43
+ const TRUTHY = ["1", "true", "yes", "on"];
44
+ const FALSEY = ["0", "false", "no", "off", ""];
45
+
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());
51
+ }
52
+
53
+ // Select which tools to register based on env config.
54
+ //
55
+ // Precedence:
56
+ // 1. Base set:
57
+ // - GITFLIC_MCP_TOOLS (explicit allowlist), if set; else
58
+ // - GITFLIC_MCP_GROUPS (by group), if set; else
59
+ // - all tools.
60
+ // 2. GITFLIC_MCP_READONLY truthy → keep only read-only tools (overlay).
61
+ // 3. GITFLIC_MCP_EXCLUDE → drop named tools (applied last, always wins).
62
+ //
63
+ // Returns { selected, warnings } — warnings are human-readable strings the
64
+ // caller prints to stderr. Unknown names/groups are warned about, not fatal.
65
+ export function selectTools(tools, env = {}) {
66
+ const warnings = [];
67
+ const allGroups = groupsOf(tools);
68
+ const byName = new Map(tools.map((t) => [t.name, t]));
69
+
70
+ // Tool names are all lowercase, so match TOOLS/EXCLUDE case-insensitively
71
+ // too (consistent with GROUPS). Otherwise a mis-cased EXCLUDE of a write
72
+ // tool would silently keep it registered.
73
+ const wantTools = parseList(env.GITFLIC_MCP_TOOLS, { lower: true });
74
+ const wantGroups = parseList(env.GITFLIC_MCP_GROUPS, { lower: true });
75
+ const exclude = parseList(env.GITFLIC_MCP_EXCLUDE, { lower: true });
76
+
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
+ }
87
+
88
+ let base;
89
+ if (wantTools.length) {
90
+ if (wantGroups.length) {
91
+ warnings.push(
92
+ "GITFLIC_MCP_TOOLS is set, so GITFLIC_MCP_GROUPS is ignored (explicit tool list wins).",
93
+ );
94
+ }
95
+ for (const n of wantTools) {
96
+ if (!byName.has(n)) {
97
+ warnings.push(`GITFLIC_MCP_TOOLS: unknown tool "${n}" (ignored).`);
98
+ }
99
+ }
100
+ base = tools.filter((t) => wantTools.includes(t.name));
101
+ } else if (wantGroups.length) {
102
+ for (const g of wantGroups) {
103
+ if (!allGroups.includes(g)) {
104
+ warnings.push(
105
+ `GITFLIC_MCP_GROUPS: unknown group "${g}" (ignored). Valid groups: ${allGroups.join(", ")}.`,
106
+ );
107
+ }
108
+ }
109
+ base = tools.filter((t) => wantGroups.includes(toolGroup(t.name)));
110
+ } else {
111
+ base = tools.slice();
112
+ }
113
+
114
+ if (readonly) {
115
+ const before = base.length;
116
+ base = base.filter(isReadOnly);
117
+ if (before > 0 && base.length === 0) {
118
+ warnings.push(
119
+ "GITFLIC_MCP_READONLY removed every selected tool (the selection contained only write tools).",
120
+ );
121
+ }
122
+ }
123
+
124
+ if (exclude.length) {
125
+ for (const n of exclude) {
126
+ if (!byName.has(n)) {
127
+ warnings.push(`GITFLIC_MCP_EXCLUDE: unknown tool "${n}" (ignored).`);
128
+ }
129
+ }
130
+ base = base.filter((t) => !exclude.includes(t.name));
131
+ }
132
+
133
+ if (base.length === 0) {
134
+ warnings.push(
135
+ "Tool filter matched 0 tools — the server will start with no tools registered. Check your GITFLIC_MCP_* settings.",
136
+ );
137
+ }
138
+
139
+ return { selected: base, warnings };
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitflic-cli-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": {
@@ -10,6 +10,7 @@
10
10
  "server.mjs",
11
11
  "runner.mjs",
12
12
  "tools.mjs",
13
+ "filter.mjs",
13
14
  "project-resolver.mjs",
14
15
  "README.md"
15
16
  ],
package/server.mjs CHANGED
@@ -21,6 +21,7 @@ import { z } from "zod";
21
21
 
22
22
  import { runGitflic, runGitflicJson, GitflicError } from "./runner.mjs";
23
23
  import { TOOLS } from "./tools.mjs";
24
+ import { selectTools } from "./filter.mjs";
24
25
  import { resolveProjectArg } from "./project-resolver.mjs";
25
26
 
26
27
  const server = new McpServer(
@@ -66,8 +67,15 @@ function jsonSchemaToZod(schema) {
66
67
  return z.object(props);
67
68
  }
68
69
 
69
- // Register each tool from TOOLS.
70
- for (const tool of TOOLS) {
70
+ // Optionally scope the tool set via env (GITFLIC_MCP_GROUPS / _READONLY /
71
+ // _TOOLS / _EXCLUDE) to keep the model's context small. Default: all tools.
72
+ const { selected, warnings } = selectTools(TOOLS, process.env);
73
+ for (const w of warnings) {
74
+ process.stderr.write(`[gitflic-mcp] WARN: ${w}\n`);
75
+ }
76
+
77
+ // Register each selected tool.
78
+ for (const tool of selected) {
71
79
  const zodInput = jsonSchemaToZod(tool.inputSchema);
72
80
  // Per MCP 2025-06-18 spec, server.tool() in @modelcontextprotocol/sdk@1.x
73
81
  // takes annotations as the 5th positional arg. Clients like Claude Code
@@ -124,5 +132,5 @@ await server.connect(transport);
124
132
 
125
133
  // Announce to stderr (stdout is reserved for JSON-RPC).
126
134
  process.stderr.write(
127
- `[gitflic-mcp] started; registered ${TOOLS.length} tools. GITFLIC_TOKEN=${process.env.GITFLIC_TOKEN ? "set" : "MISSING"}\n`,
135
+ `[gitflic-mcp] started; registered ${selected.length}/${TOOLS.length} tools. GITFLIC_TOKEN=${process.env.GITFLIC_TOKEN ? "set" : "MISSING"}\n`,
128
136
  );
package/tools.mjs CHANGED
@@ -420,8 +420,8 @@ export const TOOLS = [
420
420
  handler: (a) => ["mr", "discuss", "resolve", a.id, a.discussionUuid, "--project", a.project, "--format", "json"],
421
421
  annotations: {
422
422
  title: "Mr Discuss Resolve",
423
- readOnlyHint: true,
424
- destructiveHint: false,
423
+ readOnlyHint: false,
424
+ destructiveHint: true,
425
425
  idempotentHint: true,
426
426
  openWorldHint: true,
427
427
  },
@@ -751,8 +751,8 @@ export const TOOLS = [
751
751
  handler: () => ["project", "list", "--scope", "my"],
752
752
  annotations: {
753
753
  title: "Project List My",
754
- readOnlyHint: false,
755
- destructiveHint: true,
754
+ readOnlyHint: true,
755
+ destructiveHint: false,
756
756
  idempotentHint: false,
757
757
  openWorldHint: true,
758
758
  },
@@ -859,8 +859,8 @@ export const TOOLS = [
859
859
  },
860
860
  annotations: {
861
861
  title: "Mr By Commit",
862
- readOnlyHint: false,
863
- destructiveHint: true,
862
+ readOnlyHint: true,
863
+ destructiveHint: false,
864
864
  idempotentHint: false,
865
865
  openWorldHint: true,
866
866
  },