@shaohui_jin/git-cockpit-mcp-server 0.1.23 → 0.1.25
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 +31 -7
- package/dist/{chunk-S7EIN5TE.js → chunk-563C4WZJ.js} +100 -32
- package/dist/chunk-563C4WZJ.js.map +1 -0
- package/dist/{chunk-JSX53BCR.js → chunk-FDPM74UE.js} +220 -27
- package/dist/chunk-FDPM74UE.js.map +1 -0
- package/dist/chunk-QPNAXPTK.js +406 -0
- package/dist/chunk-QPNAXPTK.js.map +1 -0
- package/dist/cli-entry.js +3 -3
- package/dist/cli-entry.js.map +1 -1
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +18 -7
- package/dist/index.js +9 -3
- package/dist/mcpServer-5C8WQYy6.d.ts +44 -0
- package/dist/mcpServer.d.ts +4 -22
- package/dist/mcpServer.js +1 -1
- package/dist/{runtime-CtZr-J1V.d.ts → runtime-BHX-b-Hx.d.ts} +3 -1
- package/dist/web/assets/{index-CD99XNHB.css → index-CcJ_rkpc.css} +1 -1
- package/dist/web/assets/index-DFpy0R8H.js +413 -0
- package/dist/web/index.html +3 -3
- package/dist/webServer.d.ts +1 -1
- package/dist/webServer.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-JSX53BCR.js.map +0 -1
- package/dist/chunk-QIERTWP6.js +0 -86
- package/dist/chunk-QIERTWP6.js.map +0 -1
- package/dist/chunk-S7EIN5TE.js.map +0 -1
- package/dist/web/assets/index-D3EUbWRt.js +0 -413
package/README.md
CHANGED
|
@@ -22,12 +22,13 @@ npm install -g @shaohui_jin/git-cockpit-mcp-server
|
|
|
22
22
|
```bash
|
|
23
23
|
git-cockpit start # 启动常驻服务:Web UI + MCP Server(Streamable HTTP),默认 http://localhost:3000
|
|
24
24
|
git-cockpit mcp # 以 stdio 模式运行 MCP Server(供 Claude Desktop / Cursor 等客户端连接)
|
|
25
|
+
# 已有常驻服务时自动桥接过去,多客户端共用一个后端
|
|
25
26
|
git-cockpit version # 输出版本号
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
启动后浏览器访问 `http://localhost:3000` 即可使用 Web 界面;`/mcp` 端点提供 MCP Streamable HTTP 接入。
|
|
29
30
|
|
|
30
|
-
### 接入 MCP 客户端(stdio
|
|
31
|
+
### 接入 MCP 客户端(stdio,推荐)
|
|
31
32
|
|
|
32
33
|
以 Claude Desktop / Cursor 为例,在 MCP 配置中加入:
|
|
33
34
|
|
|
@@ -42,7 +43,28 @@ git-cockpit version # 输出版本号
|
|
|
42
43
|
}
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
**唯一 daemon 机制**:`git-cockpit mcp` 探活到已有 daemon 就转发 stdio 请求;没有 daemon 则由当前命令启动一个 daemon,再桥接到它。
|
|
47
|
+
客户端 stdin 关闭时,如果 daemon 是本命令启动的,会一并关闭;桥接失败不会静默创建第二套 Runtime。多个客户端因此可以稳定共用同一个 Git Cockpit、
|
|
48
|
+
任务队列与仓库锁,客户端配置里不需要写密钥。多仓库时先读取 `git-cockpit://repos`,再调用
|
|
49
|
+
`git_repo_select({ "repoId": 1 })` 绑定当前 MCP session;后续省略仓库参数时不会串到其他客户端的仓库。
|
|
50
|
+
|
|
51
|
+
### 接入 MCP 客户端(Streamable HTTP)
|
|
52
|
+
|
|
53
|
+
`/mcp` 与 `/api/*` 一样受本机密钥保护,URL 方式**必须带请求头**:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"git-cockpit-http": {
|
|
59
|
+
"url": "http://localhost:3000/mcp",
|
|
60
|
+
"headers": { "X-Git-Cockpit-Secret": "<你的 localSecret>" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
密钥在 `~/.git-cockpit/config.json` 的 `auth.localSecret`,也可在 Web 界面的「设置」里点 **复制本机访问密钥**。
|
|
67
|
+
密钥不对会得到 **401**;手写请求时 `Accept` 必须同时含 `application/json` 与 `text/event-stream`,否则 **406**。
|
|
46
68
|
|
|
47
69
|
**不要**和 Git Insight(或其它会直接改同一仓的 Git MCP)同时用于同一个仓库。
|
|
48
70
|
|
|
@@ -63,15 +85,17 @@ git-cockpit version # 输出版本号
|
|
|
63
85
|
|
|
64
86
|
宿主可插入这四条 Prompt(`safe_commit` / `merge_preview_apply` / `workspace_continue` / `open_mr`),正文即上面四段。只读状态也可读 Resource:`git-cockpit://repos`、`git-cockpit://repo/current`、`git-cockpit://jobs`、`git-cockpit://jobs/{id}`。
|
|
65
87
|
|
|
88
|
+
多仓库时先读 `git-cockpit://repos` 拿 `repoId`,再 `git_repo_select({ "repoId": 1 })` 绑定当前 MCP session;之后省略 `repoId/repoPath` 的工具都作用在这个仓。要临时操作别的仓,单次调用传 `repoId` 即可,不会改变绑定。
|
|
89
|
+
|
|
66
90
|
默认只给摘要;要某文件正文加 `path` 或 `detail=true`。
|
|
67
91
|
|
|
68
92
|
### 环境变量
|
|
69
93
|
|
|
70
|
-
| 变量
|
|
71
|
-
|
|
|
94
|
+
| 变量 | 说明 | 默认 |
|
|
95
|
+
| ---------------------- | ------------------------------------ | ---------------- |
|
|
72
96
|
| `GIT_COCKPIT_DATA_DIR` | 数据目录(仓库列表、审计日志、备份) | `~/.git-cockpit` |
|
|
73
|
-
| `GIT_COCKPIT_PORT`
|
|
74
|
-
| `GIT_COCKPIT_HOST`
|
|
97
|
+
| `GIT_COCKPIT_PORT` | Web 服务端口 | `3000` |
|
|
98
|
+
| `GIT_COCKPIT_HOST` | 监听地址 | `localhost` |
|
|
75
99
|
|
|
76
100
|
## 主要特性
|
|
77
101
|
|
|
@@ -96,4 +120,4 @@ git-cockpit version # 输出版本号
|
|
|
96
120
|
|
|
97
121
|
## License
|
|
98
122
|
|
|
99
|
-
MIT
|
|
123
|
+
MIT
|
|
@@ -4,12 +4,13 @@ import {
|
|
|
4
4
|
McpHttpHandler,
|
|
5
5
|
TOOL_DEFS,
|
|
6
6
|
TOOL_DEF_MAP,
|
|
7
|
+
collectRepoOverviewForId,
|
|
7
8
|
collectRepoOverviews,
|
|
8
9
|
executeTool,
|
|
9
10
|
summarizeForAgent,
|
|
10
11
|
toolSummaries,
|
|
11
12
|
version
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-FDPM74UE.js";
|
|
13
14
|
|
|
14
15
|
// src/webServer.ts
|
|
15
16
|
import * as path3 from "path";
|
|
@@ -66,7 +67,13 @@ import {
|
|
|
66
67
|
import * as fs from "fs";
|
|
67
68
|
import * as path from "path";
|
|
68
69
|
import { randomBytes } from "crypto";
|
|
69
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
DEFAULT_CONFIG,
|
|
72
|
+
expandHome,
|
|
73
|
+
normalizeLlmConfig,
|
|
74
|
+
normalizeMrConfig,
|
|
75
|
+
normalizePermissions
|
|
76
|
+
} from "@shaohui_jin/git-cockpit-core";
|
|
70
77
|
function deepMerge(target, source) {
|
|
71
78
|
if (source === null || source === void 0) return target;
|
|
72
79
|
if (Array.isArray(source)) {
|
|
@@ -168,11 +175,7 @@ var ConfigStore = class {
|
|
|
168
175
|
// src/repoManager.ts
|
|
169
176
|
import "events";
|
|
170
177
|
import * as path2 from "path";
|
|
171
|
-
import {
|
|
172
|
-
assertRepoAllowed,
|
|
173
|
-
GitService,
|
|
174
|
-
RepoNotFoundError
|
|
175
|
-
} from "@shaohui_jin/git-cockpit-core";
|
|
178
|
+
import { assertRepoAllowed, GitService, RepoNotFoundError } from "@shaohui_jin/git-cockpit-core";
|
|
176
179
|
var RepoManager = class {
|
|
177
180
|
constructor(options) {
|
|
178
181
|
this.options = options;
|
|
@@ -185,6 +188,7 @@ var RepoManager = class {
|
|
|
185
188
|
/** repo 根路径(规范化绝对路径) -> repoId */
|
|
186
189
|
pathToId = /* @__PURE__ */ new Map();
|
|
187
190
|
idToService = /* @__PURE__ */ new Map();
|
|
191
|
+
opening = /* @__PURE__ */ new Map();
|
|
188
192
|
eventBus;
|
|
189
193
|
list() {
|
|
190
194
|
return this.options.repoStore.list().map((r) => ({
|
|
@@ -213,20 +217,38 @@ var RepoManager = class {
|
|
|
213
217
|
throw err;
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
|
-
/**
|
|
220
|
+
/** 打开仓库:同一路径并发请求共享同一个 GitService 实例。 */
|
|
217
221
|
async open(repoPath) {
|
|
222
|
+
const requestedKey = normalizePath(repoPath);
|
|
223
|
+
const inFlight = this.opening.get(requestedKey);
|
|
224
|
+
if (inFlight) return inFlight;
|
|
225
|
+
const task = this.openInternal(repoPath, requestedKey);
|
|
226
|
+
this.opening.set(requestedKey, task);
|
|
227
|
+
try {
|
|
228
|
+
return await task;
|
|
229
|
+
} finally {
|
|
230
|
+
this.opening.delete(requestedKey);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async openInternal(repoPath, requestedKey) {
|
|
234
|
+
const knownId = this.pathToId.get(requestedKey);
|
|
235
|
+
if (knownId !== void 0 && knownId !== 0) {
|
|
236
|
+
const known = await this.getById(knownId);
|
|
237
|
+
if (known) {
|
|
238
|
+
this.options.repoStore.open(known.service.repoPath);
|
|
239
|
+
return { service: known.service, record: this.options.repoStore.getById(knownId) ?? known.record };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
218
242
|
const service = await GitService.open(repoPath);
|
|
219
243
|
assertRepoAllowed(service.repoPath, this.options.getConfig().git.allowedRepos);
|
|
220
244
|
const root = service.repoPath;
|
|
221
245
|
const key = normalizePath(root);
|
|
222
246
|
const existingId = this.pathToId.get(key);
|
|
223
247
|
if (existingId !== void 0 && existingId !== 0) {
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
248
|
+
const existing = await this.getById(existingId);
|
|
249
|
+
if (existing) {
|
|
226
250
|
this.options.repoStore.open(root);
|
|
227
|
-
this.
|
|
228
|
-
this.attach(service);
|
|
229
|
-
return { service, record: this.options.repoStore.getById(record2.id) ?? record2 };
|
|
251
|
+
return { service: existing.service, record: this.options.repoStore.getById(existingId) ?? existing.record };
|
|
230
252
|
}
|
|
231
253
|
}
|
|
232
254
|
const record = this.options.repoStore.open(root);
|
|
@@ -356,8 +378,10 @@ import swaggerUi from "@fastify/swagger-ui";
|
|
|
356
378
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
357
379
|
var REST_META = {
|
|
358
380
|
"get /api/health": { summary: "\u63A2\u6D3B", tags: ["\u7CFB\u7EDF"] },
|
|
381
|
+
"post /api/shutdown": { summary: "\u4F18\u96C5\u9000\u51FA daemon\uFF08\u672C\u673A\u5BC6\u94A5\uFF1B\u6709 running \u4EFB\u52A1\u65F6 409\uFF09", tags: ["\u7CFB\u7EDF"] },
|
|
359
382
|
"get /api/repos": { summary: "\u5DF2\u6253\u5F00\u4ED3\u5E93\u5217\u8868", tags: ["\u4ED3\u5E93"] },
|
|
360
|
-
"get /api/repos/overview": { summary: "\u5DF2\u6253\u5F00\u4ED3\u5E93\u8109\u640F\uFF08\u5DE5\u4F5C\u53F0\uFF09", tags: ["\u4ED3\u5E93"] },
|
|
383
|
+
"get /api/repos/overview": { summary: "\u5DF2\u6253\u5F00\u4ED3\u5E93\u8109\u640F\uFF08\u5DE5\u4F5C\u53F0\uFF0C\u5168\u91CF\uFF09", tags: ["\u4ED3\u5E93"] },
|
|
384
|
+
"get /api/repos/{id}/overview": { summary: "\u5355\u4E2A\u4ED3\u5E93\u8109\u640F\uFF08\u5DE5\u4F5C\u53F0\u589E\u91CF\uFF09", tags: ["\u4ED3\u5E93"] },
|
|
361
385
|
"post /api/jobs": { summary: "\u63D0\u4EA4\u540E\u53F0\u4EFB\u52A1\uFF08clone / survey / fetch\uFF09", tags: ["\u4ED3\u5E93"] },
|
|
362
386
|
"post /api/repos/open": { summary: "\u6253\u5F00\u672C\u5730\u4ED3\u5E93", tags: ["\u4ED3\u5E93"] },
|
|
363
387
|
"delete /api/repos/{id}": { summary: "\u5173\u95ED\u5E76\u79FB\u9664\u4ED3\u5E93", tags: ["\u4ED3\u5E93"] },
|
|
@@ -736,11 +760,15 @@ async function confirmChatWrite(runtime, gate, token) {
|
|
|
736
760
|
if (!gate.previewsMatch(ticket.preview, fresh)) {
|
|
737
761
|
return { ok: false, error: "\u4ED3\u5E93\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u540E\u518D\u786E\u8BA4", tool: ticket.tool, preview: fresh };
|
|
738
762
|
}
|
|
739
|
-
const exec = await executeTool(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
763
|
+
const exec = await executeTool(
|
|
764
|
+
def,
|
|
765
|
+
{ ...ticket.args, dryRun: false, repoPath: ticket.repoPath },
|
|
766
|
+
{
|
|
767
|
+
runtime,
|
|
768
|
+
source: "chat",
|
|
769
|
+
repoPath: ticket.repoPath
|
|
770
|
+
}
|
|
771
|
+
);
|
|
744
772
|
if (!exec.success) {
|
|
745
773
|
return { ok: false, error: exec.error?.message ?? "\u6267\u884C\u5931\u8D25", tool: ticket.tool };
|
|
746
774
|
}
|
|
@@ -967,6 +995,20 @@ async function createWebServer(runtime, options = {}) {
|
|
|
967
995
|
}
|
|
968
996
|
return { secret: runtime.config.auth.localSecret };
|
|
969
997
|
});
|
|
998
|
+
app.post("/api/shutdown", async (_req, reply) => {
|
|
999
|
+
if (jobs.list().some((j) => j.status === "running")) {
|
|
1000
|
+
return reply.code(409).send({ error: "\u6709\u4EFB\u52A1\u8FDB\u884C\u4E2D\uFF0C\u8BF7\u5148\u7B49\u5F85\u7ED3\u675F" });
|
|
1001
|
+
}
|
|
1002
|
+
await reply.code(204).send();
|
|
1003
|
+
setImmediate(() => {
|
|
1004
|
+
void (async () => {
|
|
1005
|
+
mcpHttp.dispose();
|
|
1006
|
+
await app.close();
|
|
1007
|
+
disposeRuntime(runtime);
|
|
1008
|
+
if (!process.env.VITEST) process.exit(0);
|
|
1009
|
+
})();
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
970
1012
|
app.get("/api/jobs", async () => ({
|
|
971
1013
|
jobs: jobs.list().map((j) => jobs.summary(j))
|
|
972
1014
|
}));
|
|
@@ -1070,6 +1112,17 @@ async function createWebServer(runtime, options = {}) {
|
|
|
1070
1112
|
app.get("/api/repos/overview", async () => ({
|
|
1071
1113
|
repos: await collectRepoOverviews(runtime)
|
|
1072
1114
|
}));
|
|
1115
|
+
app.get("/api/repos/:id/overview", async (req, reply) => {
|
|
1116
|
+
const id = Number(req.params.id);
|
|
1117
|
+
if (!Number.isInteger(id)) {
|
|
1118
|
+
return reply.code(400).send({ error: "\u975E\u6CD5\u4ED3\u5E93 id" });
|
|
1119
|
+
}
|
|
1120
|
+
const overview = await collectRepoOverviewForId(runtime, id);
|
|
1121
|
+
if (!overview) {
|
|
1122
|
+
return reply.code(404).send({ error: "\u4ED3\u5E93\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548" });
|
|
1123
|
+
}
|
|
1124
|
+
return { overview };
|
|
1125
|
+
});
|
|
1073
1126
|
app.get("/api/repos", async () => ({
|
|
1074
1127
|
repos: runtime.repoManager.list()
|
|
1075
1128
|
}));
|
|
@@ -1198,7 +1251,11 @@ async function createWebServer(runtime, options = {}) {
|
|
|
1198
1251
|
);
|
|
1199
1252
|
app.get(
|
|
1200
1253
|
"/api/repos/:id/graph",
|
|
1201
|
-
async (req, reply) => withRepo(
|
|
1254
|
+
async (req, reply) => withRepo(
|
|
1255
|
+
req,
|
|
1256
|
+
reply,
|
|
1257
|
+
async ({ service }) => service.getGraph(req.query.maxCount ? Number(req.query.maxCount) : 500)
|
|
1258
|
+
)
|
|
1202
1259
|
);
|
|
1203
1260
|
app.get(
|
|
1204
1261
|
"/api/repos/:id/branch-graph",
|
|
@@ -1214,7 +1271,11 @@ async function createWebServer(runtime, options = {}) {
|
|
|
1214
1271
|
);
|
|
1215
1272
|
app.get(
|
|
1216
1273
|
"/api/repos/:id/reflog",
|
|
1217
|
-
async (req, reply) => withRepo(
|
|
1274
|
+
async (req, reply) => withRepo(
|
|
1275
|
+
req,
|
|
1276
|
+
reply,
|
|
1277
|
+
async ({ service }) => service.getReflog(req.query.maxCount ? Number(req.query.maxCount) : 50)
|
|
1278
|
+
)
|
|
1218
1279
|
);
|
|
1219
1280
|
app.get(
|
|
1220
1281
|
"/api/repos/:id/backups",
|
|
@@ -1362,17 +1423,20 @@ async function createWebServer(runtime, options = {}) {
|
|
|
1362
1423
|
})
|
|
1363
1424
|
};
|
|
1364
1425
|
});
|
|
1365
|
-
app.post(
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1426
|
+
app.post(
|
|
1427
|
+
"/api/settings/mr-template/parse",
|
|
1428
|
+
async (req, reply) => {
|
|
1429
|
+
const markdown = typeof req.body?.markdown === "string" ? req.body.markdown : "";
|
|
1430
|
+
if (!markdown.trim()) return reply.code(400).send({ error: "\u8BF7\u63D0\u4F9B Markdown \u6587\u672C" });
|
|
1431
|
+
try {
|
|
1432
|
+
const template = parseMrTemplateMarkdown(markdown, req.body?.filename ?? "");
|
|
1433
|
+
return { template, preview: renderMrTemplate(template) };
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1436
|
+
return reply.code(400).send({ error: message });
|
|
1437
|
+
}
|
|
1374
1438
|
}
|
|
1375
|
-
|
|
1439
|
+
);
|
|
1376
1440
|
app.post(
|
|
1377
1441
|
"/api/settings/mr-template/preview",
|
|
1378
1442
|
async (req, reply) => {
|
|
@@ -1546,6 +1610,10 @@ data: ${JSON.stringify(payload)}
|
|
|
1546
1610
|
reply.hijack();
|
|
1547
1611
|
await mcpHttp.handle(req.raw, reply.raw, req.body);
|
|
1548
1612
|
});
|
|
1613
|
+
app.delete("/mcp", { schema: { hide: true } }, async (req, reply) => {
|
|
1614
|
+
reply.hijack();
|
|
1615
|
+
await mcpHttp.handle(req.raw, reply.raw, void 0);
|
|
1616
|
+
});
|
|
1549
1617
|
if (staticDir) {
|
|
1550
1618
|
app.setNotFoundHandler(async (req, reply) => {
|
|
1551
1619
|
if (req.url.startsWith("/api") || req.url.startsWith("/mcp") || req.url.startsWith("/docs")) {
|
|
@@ -1726,4 +1794,4 @@ export {
|
|
|
1726
1794
|
disposeRuntime,
|
|
1727
1795
|
createWebServer
|
|
1728
1796
|
};
|
|
1729
|
-
//# sourceMappingURL=chunk-
|
|
1797
|
+
//# sourceMappingURL=chunk-563C4WZJ.js.map
|