@kk-irving/knowledge-mcp-server 1.0.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/dist/analyze-issue.js +392 -0
- package/dist/aosp/chunker.js +221 -0
- package/dist/aosp/embed-aosp.js +56 -0
- package/dist/aosp/indexer.js +186 -0
- package/dist/aosp/module-map-loader.js +176 -0
- package/dist/aosp/search.js +254 -0
- package/dist/config.js +39 -0
- package/dist/db.js +244 -0
- package/dist/embed-pending.js +81 -0
- package/dist/embedder.js +76 -0
- package/dist/index-store.js +175 -0
- package/dist/index.js +166 -0
- package/dist/search.js +274 -0
- package/dist/sources/confluence-sync.js +180 -0
- package/dist/sources/gerrit-sync.js +180 -0
- package/dist/sources/zmind-sync.js +106 -0
- package/package.json +37 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerrit 同步:拉 changes 写入本地 gerrit_changes 表。
|
|
3
|
+
*
|
|
4
|
+
* 双通道认证(与 gerrit-mcp v1.1.0 一致):
|
|
5
|
+
* - session 模式(首选):GERRIT_AUTH_HEADER + GERRIT_COOKIE,走 non-/a/ 路径
|
|
6
|
+
* - basic 模式(备选):GERRIT_USERNAME + GERRIT_HTTP_PASSWORD,走 /a/ 路径
|
|
7
|
+
*
|
|
8
|
+
* Gerrit 响应首行的 `)]}'` XSSI 防护前缀会被剥离。
|
|
9
|
+
*/
|
|
10
|
+
import { getDb, getSyncState, setSyncState, runInTransaction } from "../db.js";
|
|
11
|
+
const GERRIT_URL = (process.env.GERRIT_URL ?? "").replace(/\/+$/, "");
|
|
12
|
+
const GERRIT_AUTH_HEADER = (process.env.GERRIT_AUTH_HEADER ?? "").trim();
|
|
13
|
+
const GERRIT_COOKIE = (process.env.GERRIT_COOKIE ?? "").trim();
|
|
14
|
+
const GERRIT_USERNAME = (process.env.GERRIT_USERNAME ?? "").trim();
|
|
15
|
+
const GERRIT_HTTP_PASSWORD = process.env.GERRIT_HTTP_PASSWORD ?? "";
|
|
16
|
+
function authMode() {
|
|
17
|
+
if (GERRIT_AUTH_HEADER && GERRIT_COOKIE)
|
|
18
|
+
return "session";
|
|
19
|
+
if (GERRIT_USERNAME && GERRIT_HTTP_PASSWORD)
|
|
20
|
+
return "basic";
|
|
21
|
+
return "missing";
|
|
22
|
+
}
|
|
23
|
+
function buildHeaders(mode) {
|
|
24
|
+
const h = { Accept: "application/json" };
|
|
25
|
+
if (mode === "session") {
|
|
26
|
+
h.Authorization = GERRIT_AUTH_HEADER;
|
|
27
|
+
h.Cookie = GERRIT_COOKIE;
|
|
28
|
+
}
|
|
29
|
+
else if (mode === "basic") {
|
|
30
|
+
const enc = Buffer.from(`${GERRIT_USERNAME}:${GERRIT_HTTP_PASSWORD}`, "utf8").toString("base64");
|
|
31
|
+
h.Authorization = `Basic ${enc}`;
|
|
32
|
+
}
|
|
33
|
+
return h;
|
|
34
|
+
}
|
|
35
|
+
function injectAuthPrefix(p, mode) {
|
|
36
|
+
const norm = p.startsWith("/") ? p : `/${p}`;
|
|
37
|
+
if (mode === "session") {
|
|
38
|
+
if (norm.startsWith("/a/"))
|
|
39
|
+
return norm.slice(2);
|
|
40
|
+
return norm;
|
|
41
|
+
}
|
|
42
|
+
if (norm === "/a" || norm.startsWith("/a/"))
|
|
43
|
+
return norm;
|
|
44
|
+
return `/a${norm}`;
|
|
45
|
+
}
|
|
46
|
+
function stripXssi(text) {
|
|
47
|
+
if (text.startsWith(")]}'"))
|
|
48
|
+
return text.slice(4).trimStart();
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
async function gerritGet(path, params) {
|
|
52
|
+
if (!GERRIT_URL)
|
|
53
|
+
throw new Error("GERRIT_URL 未配置");
|
|
54
|
+
const mode = authMode();
|
|
55
|
+
if (mode === "missing") {
|
|
56
|
+
throw new Error("缺少 Gerrit 凭据:请配置 GERRIT_AUTH_HEADER+GERRIT_COOKIE (session) 或 GERRIT_USERNAME+GERRIT_HTTP_PASSWORD (basic)。运行 scripts/refresh-auth.* 自动生成。");
|
|
57
|
+
}
|
|
58
|
+
const apiPath = injectAuthPrefix(path, mode);
|
|
59
|
+
const url = new URL(apiPath, GERRIT_URL);
|
|
60
|
+
for (const [k, v] of params)
|
|
61
|
+
url.searchParams.append(k, v);
|
|
62
|
+
const res = await fetch(url.toString(), {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: buildHeaders(mode),
|
|
65
|
+
});
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const stripped = stripXssi(text);
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new Error(`Gerrit HTTP ${res.status} (auth_mode=${mode}): ${stripped.slice(0, 300)}`);
|
|
70
|
+
}
|
|
71
|
+
if (!stripped)
|
|
72
|
+
return [];
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(stripped);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
throw new Error(`Gerrit 响应 JSON 解析失败: ${e.message}; preview=${stripped.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function commitMessageOf(ch) {
|
|
81
|
+
for (const rev of Object.values(ch.revisions ?? {})) {
|
|
82
|
+
const msg = rev?.commit?.message ?? "";
|
|
83
|
+
if (msg)
|
|
84
|
+
return msg;
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
const UPSERT_SQL = `
|
|
89
|
+
INSERT INTO gerrit_changes (
|
|
90
|
+
change_id, number, project, branch, subject,
|
|
91
|
+
commit_message, owner_name, status, created, updated
|
|
92
|
+
) VALUES (
|
|
93
|
+
@change_id, @number, @project, @branch, @subject,
|
|
94
|
+
@commit_message, @owner_name, @status, @created, @updated
|
|
95
|
+
)
|
|
96
|
+
ON CONFLICT(change_id) DO UPDATE SET
|
|
97
|
+
number = excluded.number,
|
|
98
|
+
project = excluded.project,
|
|
99
|
+
branch = excluded.branch,
|
|
100
|
+
subject = excluded.subject,
|
|
101
|
+
commit_message = excluded.commit_message,
|
|
102
|
+
owner_name = excluded.owner_name,
|
|
103
|
+
status = excluded.status,
|
|
104
|
+
created = excluded.created,
|
|
105
|
+
updated = excluded.updated
|
|
106
|
+
`;
|
|
107
|
+
function buildQuery(args) {
|
|
108
|
+
const parts = [];
|
|
109
|
+
if (args.query)
|
|
110
|
+
parts.push(args.query);
|
|
111
|
+
if (args.project)
|
|
112
|
+
parts.push(`project:${args.project}`);
|
|
113
|
+
if (args.since)
|
|
114
|
+
parts.push(`after:"${args.since}"`);
|
|
115
|
+
if (parts.length === 0)
|
|
116
|
+
parts.push("status:open OR -status:open"); // all
|
|
117
|
+
return parts.join(" ");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 全量/增量同步 Gerrit changes。
|
|
121
|
+
*/
|
|
122
|
+
export async function syncGerrit(args = {}) {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const limit = Math.max(1, Math.min(50000, args.limit ?? 1000));
|
|
125
|
+
const stateSince = args.since ?? getSyncState("gerrit", "last_full_sync_date") ?? "";
|
|
126
|
+
const finalQuery = buildQuery({
|
|
127
|
+
since: stateSince,
|
|
128
|
+
query: args.query,
|
|
129
|
+
project: args.project,
|
|
130
|
+
});
|
|
131
|
+
const upsert = db.prepare(UPSERT_SQL);
|
|
132
|
+
function upsertMany(rows) {
|
|
133
|
+
runInTransaction(db, () => {
|
|
134
|
+
for (const r of rows)
|
|
135
|
+
upsert.run(r);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
let offset = 0;
|
|
139
|
+
let fetched = 0;
|
|
140
|
+
let upserted = 0;
|
|
141
|
+
const batchSize = 200;
|
|
142
|
+
while (fetched < limit) {
|
|
143
|
+
const remaining = limit - fetched;
|
|
144
|
+
const n = Math.min(batchSize, remaining);
|
|
145
|
+
const params = [
|
|
146
|
+
["q", finalQuery],
|
|
147
|
+
["n", String(n)],
|
|
148
|
+
["S", String(offset)],
|
|
149
|
+
["o", "CURRENT_REVISION"],
|
|
150
|
+
["o", "CURRENT_COMMIT"],
|
|
151
|
+
["o", "DETAILED_ACCOUNTS"],
|
|
152
|
+
];
|
|
153
|
+
const changes = await gerritGet("/changes/", params);
|
|
154
|
+
if (changes.length === 0)
|
|
155
|
+
break;
|
|
156
|
+
const rows = changes.map((ch) => ({
|
|
157
|
+
change_id: ch.change_id ?? String(ch._number ?? ""),
|
|
158
|
+
number: ch._number ?? null,
|
|
159
|
+
project: ch.project ?? "",
|
|
160
|
+
branch: ch.branch ?? "",
|
|
161
|
+
subject: ch.subject ?? "",
|
|
162
|
+
commit_message: commitMessageOf(ch),
|
|
163
|
+
owner_name: ch.owner?.name ?? "",
|
|
164
|
+
status: ch.status ?? "",
|
|
165
|
+
created: ch.created ?? "",
|
|
166
|
+
updated: ch.updated ?? "",
|
|
167
|
+
}));
|
|
168
|
+
upsertMany(rows);
|
|
169
|
+
fetched += changes.length;
|
|
170
|
+
upserted += changes.length;
|
|
171
|
+
offset += changes.length;
|
|
172
|
+
if (changes.length < n)
|
|
173
|
+
break;
|
|
174
|
+
if (!changes[changes.length - 1]?._more_changes)
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
178
|
+
setSyncState("gerrit", "last_full_sync_date", today);
|
|
179
|
+
return { source: "gerrit", fetched, upserted, watermark: today, query: finalQuery };
|
|
180
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zmind 同步:拉 issues 写入本地 zmind_issues 表。
|
|
3
|
+
*
|
|
4
|
+
* 复用环境变量:
|
|
5
|
+
* - ZMIND_URL (默认 https://zmind.whaletv.com)
|
|
6
|
+
* - ZMIND_API_KEY 必填
|
|
7
|
+
*/
|
|
8
|
+
import { getDb, getSyncState, setSyncState, runInTransaction } from "../db.js";
|
|
9
|
+
const ZMIND_URL = (process.env.ZMIND_URL ?? "https://zmind.whaletv.com").replace(/\/+$/, "");
|
|
10
|
+
const ZMIND_API_KEY = process.env.ZMIND_API_KEY ?? "";
|
|
11
|
+
async function zmindGet(path, params) {
|
|
12
|
+
if (!ZMIND_API_KEY)
|
|
13
|
+
throw new Error("ZMIND_API_KEY 未配置");
|
|
14
|
+
const url = new URL(path, ZMIND_URL + "/");
|
|
15
|
+
for (const [k, v] of Object.entries(params)) {
|
|
16
|
+
if (v != null)
|
|
17
|
+
url.searchParams.set(k, v);
|
|
18
|
+
}
|
|
19
|
+
url.searchParams.set("key", ZMIND_API_KEY);
|
|
20
|
+
const res = await fetch(url.toString(), {
|
|
21
|
+
headers: { Accept: "application/json" },
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`Zmind HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
25
|
+
}
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
const UPSERT_SQL = `
|
|
29
|
+
INSERT INTO zmind_issues (
|
|
30
|
+
id, tracker, subject, description, status, assigned_to,
|
|
31
|
+
project_id, project_name, created_on, updated_on
|
|
32
|
+
) VALUES (
|
|
33
|
+
@id, @tracker, @subject, @description, @status, @assigned_to,
|
|
34
|
+
@project_id, @project_name, @created_on, @updated_on
|
|
35
|
+
)
|
|
36
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
37
|
+
tracker = excluded.tracker,
|
|
38
|
+
subject = excluded.subject,
|
|
39
|
+
description = excluded.description,
|
|
40
|
+
status = excluded.status,
|
|
41
|
+
assigned_to = excluded.assigned_to,
|
|
42
|
+
project_id = excluded.project_id,
|
|
43
|
+
project_name = excluded.project_name,
|
|
44
|
+
created_on = excluded.created_on,
|
|
45
|
+
updated_on = excluded.updated_on
|
|
46
|
+
`;
|
|
47
|
+
/**
|
|
48
|
+
* 全量/增量同步 Zmind issues。
|
|
49
|
+
*
|
|
50
|
+
* @param args.since 仅同步 updated_on >= since 的 issues(YYYY-MM-DD)。未传则用 sync_state 水位。
|
|
51
|
+
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000。
|
|
52
|
+
* @param args.statusId 状态过滤;默认 "*" 全部
|
|
53
|
+
*/
|
|
54
|
+
export async function syncZmind(args = {}) {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const limit = Math.max(1, Math.min(50000, args.limit ?? 1000));
|
|
57
|
+
const stateSince = args.since ?? getSyncState("zmind", "last_full_sync") ?? "";
|
|
58
|
+
const upsert = db.prepare(UPSERT_SQL);
|
|
59
|
+
function upsertMany(rows) {
|
|
60
|
+
runInTransaction(db, () => {
|
|
61
|
+
for (const r of rows)
|
|
62
|
+
upsert.run(r);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
let offset = 0;
|
|
66
|
+
let fetched = 0;
|
|
67
|
+
let upserted = 0;
|
|
68
|
+
const pageSize = 100;
|
|
69
|
+
while (fetched < limit) {
|
|
70
|
+
const remaining = limit - fetched;
|
|
71
|
+
const batch = Math.min(pageSize, remaining);
|
|
72
|
+
const params = {
|
|
73
|
+
status_id: args.statusId ?? "*",
|
|
74
|
+
sort: "updated_on:desc",
|
|
75
|
+
offset: String(offset),
|
|
76
|
+
limit: String(batch),
|
|
77
|
+
};
|
|
78
|
+
if (stateSince)
|
|
79
|
+
params.updated_on = `>=${stateSince}`;
|
|
80
|
+
const data = await zmindGet("/issues.json", params);
|
|
81
|
+
const issues = data.issues ?? [];
|
|
82
|
+
if (issues.length === 0)
|
|
83
|
+
break;
|
|
84
|
+
const rows = issues.map((i) => ({
|
|
85
|
+
id: i.id,
|
|
86
|
+
tracker: i.tracker?.name ?? "",
|
|
87
|
+
subject: i.subject ?? "",
|
|
88
|
+
description: i.description ?? "",
|
|
89
|
+
status: i.status?.name ?? "",
|
|
90
|
+
assigned_to: i.assigned_to?.name ?? "",
|
|
91
|
+
project_id: i.project?.id ?? null,
|
|
92
|
+
project_name: i.project?.name ?? "",
|
|
93
|
+
created_on: i.created_on ?? "",
|
|
94
|
+
updated_on: i.updated_on ?? "",
|
|
95
|
+
}));
|
|
96
|
+
upsertMany(rows);
|
|
97
|
+
fetched += issues.length;
|
|
98
|
+
upserted += issues.length;
|
|
99
|
+
offset += issues.length;
|
|
100
|
+
if (issues.length < batch)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const watermark = new Date().toISOString();
|
|
104
|
+
setSyncState("zmind", "last_full_sync", watermark);
|
|
105
|
+
return { source: "zmind", fetched, upserted, watermark };
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kk-irving/knowledge-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local knowledge base MCP Server: vector (BGE-small-zh ONNX) + FTS5 hybrid retrieval across Zmind PR / Gerrit changes / Confluence pages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"knowledge-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "tsx src/index.ts",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.5.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "1.12.1",
|
|
23
|
+
"@xenova/transformers": "^2.17.2",
|
|
24
|
+
"zod": "3.24.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "24.0.3",
|
|
28
|
+
"tsx": "4.19.4",
|
|
29
|
+
"typescript": "5.8.3"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/KK-Irving/whaletv-dev-power.git",
|
|
34
|
+
"directory": "mcp-servers/knowledge-mcp-server"
|
|
35
|
+
},
|
|
36
|
+
"license": "UNLICENSED"
|
|
37
|
+
}
|