kbs-upload-mcp 0.0.0-stage → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +49 -2
  2. package/index.mjs +216 -0
  3. package/package.json +20 -4
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # kbs-upload-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 知识库(KB)MCP 服务:让 Agent 上传技术文档、检索文档、获取项目分组清单。
4
+
5
+ 上传走 ai-service 中转接口(`POST /api/skt/kb/upload`),文件由内网服务直传
6
+ KB 私有桶并注册落库。**用户侧只需内网可达 ai-service + Skill Token,
7
+ 无需 NOS 凭据、无需 VPN。**
8
+
9
+ ## 接入准备
10
+
11
+ 1. 登录 [AI 基础服务](https://ai-service.lofter.com) → 左上导航「Skill Token」→ 新建
12
+ 2. 权限勾选:`kb-upload`(上传)、`kb-search`(检索)
13
+ 3. 前提:账号已被管理员加入至少一个租户分组
14
+
15
+ ## MCP 配置
16
+
17
+ ```jsonc
18
+ {
19
+ "mcpServers": {
20
+ "kbs-upload-mcp": {
21
+ "command": "npx",
22
+ "args": ["-y", "kbs-upload-mcp"],
23
+ "env": {
24
+ "AI_SERVICE_BASE": "https://ai-service-lofter.hz.netease.com",
25
+ "SKILL_TOKEN": "skt-xxxxxxxx"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## 工具
33
+
34
+ | 工具 | 说明 |
35
+ |---|---|
36
+ | `kb_list_projects()` | 获取项目分组清单,上传前必须先调用 |
37
+ | `kb_search(conditions, limit?)` | 检索文档;`conditions` 为 `{title?, summary?, author?, project?}`,字段间 OR |
38
+ | `kb_upload(filePath, title, summary, project, author?, tenantGroup?)` | 上传文档;摘要 80–200 字必填,返回 `{id, downloadUrl}` |
39
+
40
+ 详细使用规范(摘要撰写、错误码、FAQ)见仓库内文档
41
+ `docs/kb-mcp-usage.md`(lofter-ai-service 仓库)。
42
+
43
+ ## 环境变量
44
+
45
+ | 变量 | 含义 | 默认值 |
46
+ |---|---|---|
47
+ | `AI_SERVICE_BASE` | ai-service 地址(内网) | `http://127.0.0.1:3000` |
48
+ | `SKILL_TOKEN` | Skill Token(skt- 前缀) | 必配 |
49
+
50
+ 要求 Node.js ≥ 18(使用内置 fetch / FormData / Blob)。
package/index.mjs ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kbs-upload-mcp — 知识库(KB)专用 MCP
4
+ *
5
+ * 上传走 ai-service 中转接口(POST /api/skt/kb/upload):文件由内网服务直传
6
+ * KB 私有桶,用户侧只需内网可达 ai-service + Skill Token,
7
+ * 无需 NOS 凭据、无需 VPN 容器。
8
+ *
9
+ * 协议:MCP stdio(换行分隔 JSON-RPC 2.0),零 npm 依赖,`node index.mjs` 直接运行。
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { basename } from "node:path";
13
+
14
+ // 按扩展名推断 MIME,附在 Blob 上随 multipart 上传(服务端优先采用)
15
+ function guessMime(fileName) {
16
+ const lower = fileName.toLowerCase();
17
+ if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "text/markdown";
18
+ if (lower.endsWith(".html") || lower.endsWith(".htm")) return "text/html";
19
+ if (lower.endsWith(".pdf")) return "application/pdf";
20
+ if (lower.endsWith(".txt")) return "text/plain";
21
+ if (lower.endsWith(".png")) return "image/png";
22
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
23
+ return "application/octet-stream";
24
+ }
25
+
26
+ // ── 配置(env 注入,见工程 .mcp.json)───────────────────────────────
27
+ const AI_SERVICE_BASE = (process.env.AI_SERVICE_BASE || "http://127.0.0.1:3000").replace(/\/$/, "");
28
+ const SKILL_TOKEN = process.env.SKILL_TOKEN || "";
29
+
30
+ // ── 服务端 API 封装(Bearer + X-Skill-Token 双头:办公网 WAF 会剥 Authorization)──
31
+ function authHeaders() {
32
+ return { Authorization: `Bearer ${SKILL_TOKEN}`, "X-Skill-Token": SKILL_TOKEN };
33
+ }
34
+
35
+ async function serviceApi(path, init = {}) {
36
+ const resp = await fetch(`${AI_SERVICE_BASE}${path}`, {
37
+ ...init,
38
+ headers: {
39
+ ...authHeaders(),
40
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
41
+ ...init.headers,
42
+ },
43
+ });
44
+ const payload = await resp.json().catch(() => ({}));
45
+ if (!resp.ok) {
46
+ throw new Error(`ai-service ${resp.status}: ${payload.error || JSON.stringify(payload)}`);
47
+ }
48
+ return payload;
49
+ }
50
+
51
+ // 服务端返回的 downloadUrl 是 path(/api/kb/documents/download/...),
52
+ // 这里递归补全为 AI_SERVICE_BASE 下的完整链接,用户拿到即可直接打开
53
+ function absolutizeDownloadUrl(data) {
54
+ if (Array.isArray(data)) return data.map(absolutizeDownloadUrl);
55
+ if (data && typeof data === "object") {
56
+ return Object.fromEntries(
57
+ Object.entries(data).map(([k, v]) => [
58
+ k,
59
+ k === "downloadUrl" && typeof v === "string" && v.startsWith("/")
60
+ ? `${AI_SERVICE_BASE}${v}`
61
+ : absolutizeDownloadUrl(v),
62
+ ])
63
+ );
64
+ }
65
+ return data;
66
+ }
67
+
68
+ // ── 三个工具实现 ─────────────────────────────────────────────────────
69
+ async function kbListProjects() {
70
+ return serviceApi("/api/skt/kb/projects");
71
+ }
72
+
73
+ async function kbSearch(args) {
74
+ const sp = new URLSearchParams();
75
+ for (const k of ["title", "summary", "author", "project"]) {
76
+ if (args.conditions?.[k]) sp.set(k, String(args.conditions[k]));
77
+ }
78
+ if (args.limit) sp.set("limit", String(args.limit));
79
+ const payload = await serviceApi(`/api/skt/kb/search?${sp}`);
80
+ return absolutizeDownloadUrl(payload);
81
+ }
82
+
83
+ async function kbUpload(args) {
84
+ const { filePath, title, summary, project, author, tenantGroup } = args;
85
+ if (!filePath || !title?.trim() || !summary?.trim()) {
86
+ throw new Error("filePath / title / summary 均为必填(summary 80-200字:问题场景/核心结论/关键词)");
87
+ }
88
+ const body = readFileSync(filePath);
89
+ const fileName = basename(filePath);
90
+
91
+ // multipart 中转上传:文件 + 元数据一并交给 ai-service(内网服务直传私有桶)
92
+ const form = new FormData();
93
+ form.append("file", new Blob([body], { type: guessMime(fileName) }), fileName);
94
+ form.append("title", title.trim());
95
+ form.append("summary", summary.trim());
96
+ if (project?.trim()) form.append("project", project.trim());
97
+ if (author?.trim()) form.append("author", author.trim());
98
+ if (tenantGroup?.trim()) form.append("tenantGroup", tenantGroup.trim());
99
+
100
+ const resp = await fetch(`${AI_SERVICE_BASE}/api/skt/kb/upload`, {
101
+ method: "POST",
102
+ headers: authHeaders(),
103
+ body: form,
104
+ });
105
+ const payload = await resp.json().catch(() => ({}));
106
+ if (!resp.ok) {
107
+ throw new Error(`ai-service ${resp.status}: ${payload.error || JSON.stringify(payload)}`);
108
+ }
109
+ return { ...absolutizeDownloadUrl(payload), uploadVia: "service-relay" };
110
+ }
111
+
112
+ // ── MCP stdio 服务(换行分隔 JSON-RPC 2.0)───────────────────────────
113
+ const TOOLS = [
114
+ {
115
+ name: "kb_list_projects",
116
+ description: "获取知识库项目分组清单。上传前必须先调用,结合工程目录(package.json name / git remote)推断项目分组。",
117
+ inputSchema: { type: "object", properties: {} },
118
+ },
119
+ {
120
+ name: "kb_search",
121
+ description: "检索知识库文档。conditions 为 {title?, summary?, author?, project?},已填字段之间是并集(OR)。回答中请附返回的 downloadUrl。",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ conditions: {
126
+ type: "object",
127
+ properties: {
128
+ title: { type: "string" },
129
+ summary: { type: "string" },
130
+ author: { type: "string" },
131
+ project: { type: "string" },
132
+ },
133
+ },
134
+ limit: { type: "number", description: "返回条数上限,默认 10,最大 50" },
135
+ },
136
+ },
137
+ },
138
+ {
139
+ name: "kb_upload",
140
+ description: "上传文档到知识库(经 ai-service 中转直传私有桶 + 注册落库)。必须先 kb_list_projects 确认分组、拟好 80-200 字摘要并向用户确认后再调用。返回 {id, downloadUrl}。",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ filePath: { type: "string", description: "本地文件绝对路径" },
145
+ title: { type: "string", description: "文档标题" },
146
+ summary: { type: "string", description: "摘要,80-200字:问题场景/核心结论/关键词" },
147
+ project: { type: "string", description: "项目名称(来自 kb_list_projects,缺省落 default 分组)" },
148
+ author: { type: "string", description: "文档作者,缺省为上传者本人" },
149
+ tenantGroup: { type: "string", description: "租户分组名称,缺省为上传者所属分组;不存在的新分组会自动创建并加入" },
150
+ },
151
+ required: ["filePath", "title", "summary", "project"],
152
+ },
153
+ },
154
+ ];
155
+
156
+ function rpcResult(id, result) {
157
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
158
+ }
159
+ function rpcError(id, code, message) {
160
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
161
+ }
162
+
163
+ let buffer = "";
164
+ process.stdin.setEncoding("utf8");
165
+ process.stdin.on("data", (chunk) => {
166
+ buffer += chunk;
167
+ let idx;
168
+ while ((idx = buffer.indexOf("\n")) >= 0) {
169
+ const line = buffer.slice(0, idx).trim();
170
+ buffer = buffer.slice(idx + 1);
171
+ if (line) handleMessage(line);
172
+ }
173
+ });
174
+ // stdin 关闭后不立即退出:等待在途请求完成,事件循环自然排空退出
175
+ process.stdin.on("end", () => {});
176
+
177
+ function handleMessage(line) {
178
+ let msg;
179
+ try {
180
+ msg = JSON.parse(line);
181
+ } catch {
182
+ return;
183
+ }
184
+ const { id, method, params } = msg;
185
+ if (method === "initialize") {
186
+ rpcResult(id, {
187
+ protocolVersion: params?.protocolVersion ?? "2025-06-18",
188
+ capabilities: { tools: {} },
189
+ serverInfo: { name: "kbs-upload-mcp", version: "0.2.1" },
190
+ });
191
+ } else if (method === "notifications/initialized" || method?.startsWith("notifications/")) {
192
+ // 通知无需应答
193
+ } else if (method === "tools/list") {
194
+ rpcResult(id, { tools: TOOLS });
195
+ } else if (method === "tools/call") {
196
+ const tool = params?.name;
197
+ const args = params?.arguments ?? {};
198
+ const impl = { kb_list_projects: kbListProjects, kb_search: kbSearch, kb_upload: kbUpload }[tool];
199
+ if (!impl) {
200
+ rpcError(id, -32602, `Unknown tool: ${tool}`);
201
+ return;
202
+ }
203
+ impl(args)
204
+ .then((data) => rpcResult(id, {
205
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
206
+ }))
207
+ .catch((err) => rpcResult(id, {
208
+ content: [{ type: "text", text: `上传失败: ${err instanceof Error ? err.message : String(err)}` }],
209
+ isError: true,
210
+ }));
211
+ } else if (method === "ping") {
212
+ rpcResult(id, {});
213
+ } else if (id !== undefined) {
214
+ rpcError(id, -32601, `Method not found: ${method}`);
215
+ }
216
+ }
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "kbs-upload-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "知识库(KB)MCP:上传技术文档、检索文档、获取项目分组清单(经 ai-service 中转直传私有桶,无需 NOS 凭据)",
5
+ "type": "module",
6
+ "bin": {
7
+ "kbs-upload-mcp": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "UNLICENSED",
16
+ "keywords": [
17
+ "mcp",
18
+ "kb",
19
+ "upload",
20
+ "nos"
21
+ ]
22
+ }