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 +132 -0
- package/package.json +50 -0
- package/project-resolver.mjs +56 -0
- package/runner.mjs +92 -0
- package/server.mjs +128 -0
- package/tools.mjs +1629 -0
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
|
+
);
|