gitflic-cli-mcp 0.1.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 ADDED
@@ -0,0 +1,132 @@
1
+ # gitflic-cli MCP-сервер
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io/) сервер, оборачивающий CLI `gitflic` и отдающий **77 типизированных тулзов** поверх команд GitFlic REST для AI-агентов (Claude Code, Cursor и т.п.).
4
+
5
+ ## Что внутри
6
+
7
+ - `server.mjs` — MCP-сервер на stdio, регистрирует все тулзы, диспетчеризует в CLI.
8
+ - `tools.mjs` — описания тулзов (имя, описание, JSON-схема, билдер аргументов CLI).
9
+ - `runner.mjs` — обёртка над `child_process`, запускает `gitflic` CLI (`node …/lib/gitflic.mjs`) и парсит JSON.
10
+ - зависимости: `@modelcontextprotocol/sdk`, `zod` и сам `gitflic-cli` (CLI).
11
+
12
+ MCP-сервер — это **тонкая прослойка**: каждая тулза маппится 1-в-1 на подкоманду CLI. Всё, что можно сделать через `gitflic …`, доступно и из MCP-хоста.
13
+
14
+ ## Установка
15
+
16
+ Ставить ничего не нужно — запуск через `npx`:
17
+
18
+ ```bash
19
+ GITFLIC_TOKEN="xxxx" npx -y gitflic-cli-mcp
20
+ ```
21
+
22
+ `npx` подтянет пакет, его зависимости и сам CLI `gitflic-cli`. Из исходников — вручную:
23
+
24
+ ```bash
25
+ cd mcp-server && npm install
26
+ GITFLIC_TOKEN="xxxx" node server.mjs
27
+ ```
28
+
29
+ Требуется Node 18+.
30
+
31
+ ## Настройка токена
32
+
33
+ Сервер читает `GITFLIC_TOKEN` из окружения родительского процесса — ровно так же, как CLI:
34
+
35
+ ```bash
36
+ export GITFLIC_TOKEN="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
37
+ ```
38
+
39
+ Для self-hosted GitFlic дополнительно: `export GITFLIC_API_BASE=https://git.example.com`.
40
+
41
+ ## Регистрация в MCP-хосте
42
+
43
+ ### Claude Code
44
+
45
+ ```bash
46
+ claude mcp add gitflic --env GITFLIC_TOKEN=xxxxxxxx -- npx -y gitflic-cli-mcp
47
+ ```
48
+
49
+ Или вручную в `~/.claude.json`:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "gitflic": {
55
+ "command": "npx",
56
+ "args": ["-y", "gitflic-cli-mcp"],
57
+ "env": { "GITFLIC_TOKEN": "xxxxxxxx" }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Cursor (`~/.cursor/mcp.json`)
64
+
65
+ Та же структура. Перезапустите хост. (Из исходников: `"command": "node", "args": ["/absolute/path/to/gitflic-cli/mcp-server/server.mjs"]`.)
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 …` напрямую.
91
+
92
+ ## Как это работает
93
+
94
+ ```
95
+ [Claude Code / Cursor]
96
+ │ JSON-RPC по stdio
97
+
98
+ mcp-server/server.mjs
99
+ │ тулз-вызов после zod-валидации
100
+
101
+ mcp-server/runner.mjs ── spawn ──► node …/gitflic-cli/lib/gitflic.mjs mr create --title "…" --format json
102
+
103
+
104
+ распарсенный JSON-ответ
105
+ ```
106
+
107
+ Каждый хендлер тулза возвращает `["gitflic", "subcommand", "--flag", "value", …, "--format", "json"]`. Runner форсит добавление `--format json` в конец, парсит и возвращает структурированные данные.
108
+
109
+ При ненулевом коде выхода (HTTP 422, 403, 404) тулз возвращает `isError: true` с текстом ошибки от gitflic — агент видит сбой и может восстановиться.
110
+
111
+ ## Расширение
112
+
113
+ Добавьте тулз, дописав в `TOOLS` в `tools.mjs`:
114
+
115
+ ```js
116
+ {
117
+ name: "gitflic_release_delete",
118
+ description: "Delete a release by uuid.",
119
+ inputSchema: {
120
+ type: "object",
121
+ required: ["project", "uuid"],
122
+ properties: { project: { type: "string" }, uuid: { type: "string" } },
123
+ },
124
+ handler: (a) => ["release", "delete", a.uuid, "--project", a.project, "--format", "json"],
125
+ }
126
+ ```
127
+
128
+ Отдельного кода регистрации нет — цикл в `server.mjs` подхватит тулз автоматически.
129
+
130
+ ## Лицензия
131
+
132
+ MIT (как у родительского проекта).
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "gitflic-cli-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server wrapping the gitflic CLI — exposes the GitFlic REST API as typed tools for AI agents (Claude Code, Cursor, etc.)",
5
+ "type": "module",
6
+ "bin": {
7
+ "gitflic-mcp": "server.mjs"
8
+ },
9
+ "files": [
10
+ "server.mjs",
11
+ "runner.mjs",
12
+ "tools.mjs",
13
+ "project-resolver.mjs",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "start": "node server.mjs"
21
+ },
22
+ "keywords": [
23
+ "gitflic",
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "ai-agent",
27
+ "claude",
28
+ "cursor",
29
+ "rest-api"
30
+ ],
31
+ "homepage": "https://cli-gitflic.ru/mcp-server.html",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://gitflic.ru/tikhon/gitflic-cli.git",
35
+ "directory": "mcp-server"
36
+ },
37
+ "bugs": {
38
+ "url": "https://cli-gitflic.ru/known-issues.html"
39
+ },
40
+ "author": "Tikhon Ustinov",
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0",
47
+ "gitflic-cli": "^0.3.0",
48
+ "zod": "^3.25.76"
49
+ }
50
+ }
@@ -0,0 +1,56 @@
1
+ // Helper: figure out which project (owner/alias) to use when an MCP tool
2
+ // is called without an explicit `project` argument.
3
+ //
4
+ // Resolution order:
5
+ // 1. process.env.GITFLIC_PROJECT (e.g. "tikhon/wave")
6
+ // 2. autodetect from `git remote get-url origin` in the current cwd
7
+ // 3. return null — let the tool/CLI surface its own error
8
+ //
9
+ // This mirrors the auto-detect that the gitflic CLI does, so MCP tools
10
+ // work without per-call boilerplate when the agent is in a clone.
11
+
12
+ import { spawnSync } from "node:child_process";
13
+
14
+ function runGit(args) {
15
+ try {
16
+ const r = spawnSync("git", args, { encoding: "utf8" });
17
+ if (r.status !== 0) return null;
18
+ const out = (r.stdout || "").trim();
19
+ return out.length ? out : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function detectFromRemote() {
26
+ const url = runGit(["config", "--get", "remote.origin.url"]);
27
+ if (!url) return null;
28
+ // Matches:
29
+ // git@gitflic.ru:owner/alias[.git]
30
+ // ssh://git@gitflic.ru[:port]/owner/alias[.git]
31
+ // https://gitflic.ru/owner/alias[.git]
32
+ // https://[subdomain.]gitflic.ru/owner/alias[.git]
33
+ const gitflicMatch = url.match(
34
+ /gitflic\.ru[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\/?$/i,
35
+ );
36
+ if (gitflicMatch) return `${gitflicMatch[1]}/${gitflicMatch[2]}`;
37
+ // Self-hosted: any host with /owner/alias path.
38
+ const genericMatch = url.match(/\/([^/]+)\/([^/\s]+?)(?:\.git)?\/?$/);
39
+ if (genericMatch) return `${genericMatch[1]}/${genericMatch[2]}`;
40
+ return null;
41
+ }
42
+
43
+ export function resolveProjectArg(args) {
44
+ if (args && typeof args.project === "string" && args.project.length) {
45
+ return args;
46
+ }
47
+ const envProj = process.env.GITFLIC_PROJECT;
48
+ if (envProj && envProj.length) {
49
+ return { ...args, project: envProj };
50
+ }
51
+ const auto = detectFromRemote();
52
+ if (auto) {
53
+ return { ...args, project: auto };
54
+ }
55
+ return args;
56
+ }
package/runner.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // Runner — thin wrapper that spawns the gitflic CLI and returns parsed JSON or text.
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import { existsSync } from "node:fs";
8
+ import process from "node:process";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Resolve the gitflic CLI's Node entrypoint, then run it with `node`.
15
+ // We deliberately avoid the bash shim (bin/gitflic) so this works on Windows
16
+ // and when the MCP server is installed standalone via `npx gitflic-cli-mcp`:
17
+ // - installed from npm: `gitflic-cli` is a dependency → resolves from node_modules
18
+ // - source checkout: mcp-server/ sits inside the CLI repo → ../lib/gitflic.mjs
19
+ // Fail fast with a clear message if neither is present, instead of a cryptic
20
+ // ENOENT on the first tool call.
21
+ function resolveCliEntry() {
22
+ try {
23
+ return require.resolve("gitflic-cli/lib/gitflic.mjs");
24
+ } catch {
25
+ const fallback = resolve(__dirname, "..", "lib", "gitflic.mjs");
26
+ if (!existsSync(fallback)) {
27
+ throw new Error(
28
+ "gitflic CLI not found: neither the 'gitflic-cli' package nor the " +
29
+ `source-checkout fallback (${fallback}) is available. Install ` +
30
+ "gitflic-cli (e.g. via `npx -y gitflic-cli-mcp`) or run from the repo.",
31
+ );
32
+ }
33
+ return fallback;
34
+ }
35
+ }
36
+ const CLI_ENTRY = resolveCliEntry();
37
+
38
+ /**
39
+ * Run a gitflic command and return {stdout, stderr, code}.
40
+ */
41
+ export function runGitflic(args, { env = {}, cwd, timeoutMs = 60_000 } = {}) {
42
+ return new Promise((resolveP, rejectP) => {
43
+ const proc = spawn(process.execPath, [CLI_ENTRY, ...args], {
44
+ env: { ...process.env, ...env },
45
+ cwd,
46
+ timeout: timeoutMs,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ let stdout = "";
50
+ let stderr = "";
51
+ proc.stdout.on("data", (d) => (stdout += d.toString()));
52
+ proc.stderr.on("data", (d) => (stderr += d.toString()));
53
+ proc.on("error", rejectP);
54
+ proc.on("close", (code) => resolveP({ stdout, stderr, code: code ?? 1 }));
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Run a gitflic command and parse JSON. Falls back to {raw: text} if not JSON.
60
+ * Throws if exit code != 0.
61
+ */
62
+ export async function runGitflicJson(args, opts = {}) {
63
+ const r = await runGitflic([...args, "--format", "json"], opts);
64
+ if (r.code !== 0) {
65
+ throw new GitflicError(r.stderr.trim() || r.stdout.trim(), r.code);
66
+ }
67
+ try {
68
+ return JSON.parse(r.stdout);
69
+ } catch {
70
+ return { raw: r.stdout };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Run a gitflic command expecting pretty text output. Returns trimmed stdout.
76
+ * Throws if exit code != 0.
77
+ */
78
+ export async function runGitflicText(args, opts = {}) {
79
+ const r = await runGitflic([...args, "--format", "pretty"], opts);
80
+ if (r.code !== 0) {
81
+ throw new GitflicError(r.stderr.trim() || r.stdout.trim(), r.code);
82
+ }
83
+ return r.stdout;
84
+ }
85
+
86
+ export class GitflicError extends Error {
87
+ constructor(message, code) {
88
+ super(message);
89
+ this.code = code;
90
+ this.name = "GitflicError";
91
+ }
92
+ }
package/server.mjs ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ // gitflic MCP server — exposes the gitflic CLI as typed MCP tools.
3
+ // Stdio transport; no HTTP. For Claude Code / Cursor / any MCP host.
4
+ //
5
+ // Usage (no install — via npx):
6
+ // GITFLIC_TOKEN="..." npx -y gitflic-cli-mcp
7
+ //
8
+ // Or from a source checkout:
9
+ // cd mcp-server && npm install && GITFLIC_TOKEN="..." node server.mjs
10
+ //
11
+ // Then register with your MCP host, e.g. for Claude Code:
12
+ // claude mcp add gitflic --env GITFLIC_TOKEN=... -- npx -y gitflic-cli-mcp
13
+ // or add to ~/.claude.json:
14
+ // { "mcpServers": { "gitflic": { "command": "npx",
15
+ // "args": ["-y", "gitflic-cli-mcp"],
16
+ // "env": { "GITFLIC_TOKEN": "..." } } } }
17
+
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+
22
+ import { runGitflic, runGitflicJson, GitflicError } from "./runner.mjs";
23
+ import { TOOLS } from "./tools.mjs";
24
+ import { resolveProjectArg } from "./project-resolver.mjs";
25
+
26
+ const server = new McpServer(
27
+ {
28
+ name: "gitflic-cli",
29
+ version: "0.1.0",
30
+ },
31
+ {
32
+ capabilities: {
33
+ tools: {},
34
+ },
35
+ },
36
+ );
37
+
38
+ // Helper: turn a JSON Schema (from our TOOLS list) into a Zod object schema.
39
+ // MCP wants zod-style schemas. We support only the subset we use:
40
+ // { type: "object", required: [...], properties: { name: { type, description, enum? } } }
41
+ function jsonSchemaToZod(schema) {
42
+ const props = {};
43
+ for (const [key, def] of Object.entries(schema.properties || {})) {
44
+ let zodType;
45
+ switch (def.type) {
46
+ case "string":
47
+ zodType = def.enum ? z.enum(def.enum) : z.string();
48
+ break;
49
+ case "number":
50
+ case "integer":
51
+ zodType = z.number();
52
+ break;
53
+ case "boolean":
54
+ zodType = z.boolean();
55
+ break;
56
+ case "array":
57
+ zodType = z.array(z.any());
58
+ break;
59
+ default:
60
+ zodType = z.any();
61
+ }
62
+ if (def.description) zodType = zodType.describe(def.description);
63
+ if (!schema.required || !schema.required.includes(key)) zodType = zodType.optional();
64
+ props[key] = zodType;
65
+ }
66
+ return z.object(props);
67
+ }
68
+
69
+ // Register each tool from TOOLS.
70
+ for (const tool of TOOLS) {
71
+ const zodInput = jsonSchemaToZod(tool.inputSchema);
72
+ // Per MCP 2025-06-18 spec, server.tool() in @modelcontextprotocol/sdk@1.x
73
+ // takes annotations as the 5th positional arg. Clients like Claude Code
74
+ // and Cursor prompt the user for confirmation when destructiveHint=true.
75
+ const ann = tool.annotations || {};
76
+ const annotations = {
77
+ title: ann.title || tool.name,
78
+ readOnlyHint: ann.readOnlyHint === true,
79
+ destructiveHint: ann.destructiveHint !== false, // default true (may modify state)
80
+ idempotentHint: ann.idempotentHint === true,
81
+ openWorldHint: ann.openWorldHint !== false, // default true (talks to GitFlic)
82
+ };
83
+ server.tool(
84
+ tool.name,
85
+ tool.description,
86
+ zodInput.shape,
87
+ annotations,
88
+ async (args) => {
89
+ try {
90
+ // Auto-resolve `project` from env or git remote if not provided.
91
+ const resolvedArgs = resolveProjectArg(args || {});
92
+ const cliArgs = tool.handler(resolvedArgs);
93
+ // All our handlers return either an array (--format json) or
94
+ // a list ending with --format pretty. We decide based on output
95
+ // content (JSON parse) or just always try JSON first.
96
+ let result;
97
+ try {
98
+ result = await runGitflicJson(cliArgs);
99
+ } catch (e) {
100
+ if (e instanceof GitflicError) {
101
+ return {
102
+ isError: true,
103
+ content: [{ type: "text", text: `gitflic error: ${e.message}` }],
104
+ };
105
+ }
106
+ throw e;
107
+ }
108
+ return {
109
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
110
+ };
111
+ } catch (err) {
112
+ return {
113
+ isError: true,
114
+ content: [{ type: "text", text: `unexpected error: ${err.message || err}` }],
115
+ };
116
+ }
117
+ },
118
+ );
119
+ }
120
+
121
+ // Boot.
122
+ const transport = new StdioServerTransport();
123
+ await server.connect(transport);
124
+
125
+ // Announce to stderr (stdout is reserved for JSON-RPC).
126
+ process.stderr.write(
127
+ `[gitflic-mcp] started; registered ${TOOLS.length} tools. GITFLIC_TOKEN=${process.env.GITFLIC_TOKEN ? "set" : "MISSING"}\n`,
128
+ );