@autobest-ui/agent 1.0.1 → 1.0.3

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.
@@ -4,46 +4,42 @@
4
4
  * RAG MCP bridge for the local PRD knowledge Express API.
5
5
  *
6
6
  * Responsibilities are deliberately limited to:
7
- * 1. Expanding file, file-array, and directory inputs into Markdown paths.
8
- * 2. Forwarding MCP tool calls to the local HTTP API.
7
+ * 1. Packaging local Markdown files and referenced images for remote ingestion.
8
+ * 2. Forwarding MCP tool calls to the configured HTTP API.
9
9
  *
10
- * Database access, Markdown parsing, embeddings, and RAG stay in Express.
10
+ * Requirement parsing, database access, embeddings, and RAG stay in Express.
11
11
  */
12
12
 
13
- import { readdir, stat } from "node:fs/promises";
14
- import path from "node:path";
15
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
- import { z } from "zod";
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { z } from 'zod';
16
+ import { buildDocuments, expandMarkdownPaths, serializeRequestBody } from './documents.js';
18
17
 
19
18
  const DEFAULT_HTTP_TIMEOUT_MS = 120_000;
20
19
 
21
20
  function apiBaseUrlFromEnvironment() {
22
21
  const configuredUrl = process.env.RAG_API_BASE_URL?.trim();
23
22
  if (!configuredUrl) {
24
- throw new Error(
25
- "RAG_API_BASE_URL is required; configure it in the MCP server environment",
26
- );
23
+ throw new Error('RAG_API_BASE_URL is required; configure it in the MCP server environment');
27
24
  }
28
25
 
29
26
  let url;
30
27
  try {
31
28
  url = new URL(configuredUrl);
32
29
  } catch {
33
- throw new Error("RAG_API_BASE_URL must be a valid URL");
30
+ throw new Error('RAG_API_BASE_URL must be a valid URL');
34
31
  }
35
- if (url.protocol !== "http:" && url.protocol !== "https:") {
36
- throw new Error("RAG_API_BASE_URL must use http or https");
32
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
33
+ throw new Error('RAG_API_BASE_URL must use http or https');
37
34
  }
38
- return url.toString().replace(/\/$/, "");
35
+ return url.toString().replace(/\/$/, '');
39
36
  }
40
37
 
41
38
  function httpTimeoutFromEnvironment() {
42
- const configuredTimeout =
43
- process.env.RAG_MCP_HTTP_TIMEOUT_MS ?? String(DEFAULT_HTTP_TIMEOUT_MS);
39
+ const configuredTimeout = process.env.RAG_MCP_HTTP_TIMEOUT_MS ?? String(DEFAULT_HTTP_TIMEOUT_MS);
44
40
  const timeout = Number(configuredTimeout);
45
41
  if (!Number.isSafeInteger(timeout) || timeout <= 0) {
46
- throw new Error("RAG_MCP_HTTP_TIMEOUT_MS must be a positive integer");
42
+ throw new Error('RAG_MCP_HTTP_TIMEOUT_MS must be a positive integer');
47
43
  }
48
44
  return timeout;
49
45
  }
@@ -52,92 +48,27 @@ const API_BASE_URL = apiBaseUrlFromEnvironment();
52
48
  const HTTP_TIMEOUT_MS = httpTimeoutFromEnvironment();
53
49
 
54
50
  const pathsSchema = z
55
- .union([
56
- z.string().min(1),
57
- z.array(z.string().min(1)).min(1),
58
- ])
59
- .describe("单个Markdown文件、Markdown文件数组或文件夹的绝对路径");
51
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
52
+ .describe('单个Markdown文件、Markdown文件数组或文件夹的绝对路径');
60
53
  const platformSchema = z
61
- .enum(["app", "california-web", "shared"])
62
- .describe("知识所属平台;必须明确指定,防止APP与加州网站需求串库");
63
- const moduleSchema = z
64
- .string()
65
- .min(1)
66
- .optional()
67
- .describe("可选业务模块标识,例如vehicle-selector");
54
+ .enum(['app', 'california-web', 'shared'])
55
+ .describe('知识所属平台;必须明确指定,防止APP与加州网站需求串库');
56
+ const moduleSchema = z.string().min(1).optional().describe('可选业务模块标识,例如vehicle-selector');
68
57
  const pageSchema = z
69
58
  .string()
70
59
  .min(1)
71
60
  .optional()
72
- .describe(
73
- "业务页面键,例如home、pl、pn、pd、name-search;录入多个文件时不要传,由后端按文件名分别推断",
74
- );
75
- const docKeySchema = z
76
- .string()
77
- .min(1)
78
- .optional()
79
- .describe("需要限定到单一文档时传knowledge_doc.doc_key绝对路径");
80
-
81
- /** Recursively collects Markdown files from a directory in stable order. */
82
- async function scanMarkdownDirectory(directoryPath) {
83
- const markdownFiles = [];
84
- const entries = await readdir(directoryPath, { withFileTypes: true });
85
- entries.sort((left, right) => left.name.localeCompare(right.name));
86
-
87
- for (const entry of entries) {
88
- const entryPath = path.join(directoryPath, entry.name);
89
- if (entry.isDirectory()) {
90
- markdownFiles.push(...(await scanMarkdownDirectory(entryPath)));
91
- } else if (entry.isFile() && path.extname(entry.name).toLowerCase() === ".md") {
92
- markdownFiles.push(path.resolve(entryPath));
93
- }
94
- }
95
- return markdownFiles;
96
- }
97
-
98
- /** Normalizes all supported path inputs to a unique array of absolute .md files. */
99
- async function expandMarkdownPaths(pathsInput) {
100
- const inputs = Array.isArray(pathsInput) ? pathsInput : [pathsInput];
101
- const markdownFiles = [];
102
-
103
- for (const input of inputs) {
104
- if (typeof input !== "string" || !input.trim()) {
105
- throw new Error("paths中的每一项都必须是非空字符串");
106
- }
107
- if (!path.isAbsolute(input.trim())) {
108
- throw new Error(`必须使用绝对路径: ${input}`);
109
- }
110
-
111
- const absolutePath = path.resolve(input.trim());
112
- const pathStat = await stat(absolutePath);
113
- if (pathStat.isDirectory()) {
114
- markdownFiles.push(...(await scanMarkdownDirectory(absolutePath)));
115
- } else if (
116
- pathStat.isFile() &&
117
- path.extname(absolutePath).toLowerCase() === ".md"
118
- ) {
119
- markdownFiles.push(absolutePath);
120
- } else {
121
- throw new Error(`路径不是Markdown文件或文件夹: ${absolutePath}`);
122
- }
123
- }
124
-
125
- const uniquePaths = [...new Set(markdownFiles)].sort((left, right) =>
126
- left.localeCompare(right),
127
- );
128
- if (uniquePaths.length === 0) {
129
- throw new Error("指定路径中没有找到任何.md文件");
130
- }
131
- return uniquePaths;
132
- }
61
+ .describe('业务页面键,例如home、pl、pn、pd、name-search;录入多个文件时不要传,由后端按文件名分别推断');
62
+ const docKeySchema = z.string().min(1).optional().describe('需要限定到单一文档时传knowledge_doc.doc_key绝对路径');
133
63
 
134
64
  /** Posts JSON to Express and returns its parsed response body. */
135
65
  async function postJson(endpoint, body) {
66
+ const serializedBody = serializeRequestBody(body);
136
67
  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
137
- method: "POST",
138
- headers: { "content-type": "application/json" },
139
- body: JSON.stringify(body),
140
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: serializedBody,
71
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
141
72
  });
142
73
  const responseText = await response.text();
143
74
  let responseBody;
@@ -148,10 +79,7 @@ async function postJson(endpoint, body) {
148
79
  }
149
80
 
150
81
  if (!response.ok) {
151
- const details =
152
- typeof responseBody === "string"
153
- ? responseBody
154
- : JSON.stringify(responseBody);
82
+ const details = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
155
83
  throw new Error(`Express后端返回HTTP ${response.status}: ${details}`);
156
84
  }
157
85
  return responseBody;
@@ -161,12 +89,12 @@ async function postJson(endpoint, body) {
161
89
  async function getJson(endpoint, query) {
162
90
  const url = new URL(`${API_BASE_URL}${endpoint}`);
163
91
  for (const [name, value] of Object.entries(query)) {
164
- if (value === undefined || value === null || value === "") continue;
92
+ if (value === undefined || value === null || value === '') continue;
165
93
  url.searchParams.set(name, value);
166
94
  }
167
95
  const response = await fetch(url, {
168
- method: "GET",
169
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
96
+ method: 'GET',
97
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
170
98
  });
171
99
  const responseText = await response.text();
172
100
  let responseBody;
@@ -176,10 +104,7 @@ async function getJson(endpoint, query) {
176
104
  responseBody = responseText;
177
105
  }
178
106
  if (!response.ok) {
179
- const details =
180
- typeof responseBody === "string"
181
- ? responseBody
182
- : JSON.stringify(responseBody);
107
+ const details = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
183
108
  throw new Error(`Express后端返回HTTP ${response.status}: ${details}`);
184
109
  }
185
110
  return responseBody;
@@ -189,10 +114,10 @@ function toolSuccess(payload) {
189
114
  return {
190
115
  content: [
191
116
  {
192
- type: "text",
193
- text: JSON.stringify(payload, null, 2),
194
- },
195
- ],
117
+ type: 'text',
118
+ text: JSON.stringify(payload, null, 2)
119
+ }
120
+ ]
196
121
  };
197
122
  }
198
123
 
@@ -200,154 +125,138 @@ function toolError(error) {
200
125
  const message = error instanceof Error ? error.message : String(error);
201
126
  return {
202
127
  isError: true,
203
- content: [{ type: "text", text: message }],
128
+ content: [{ type: 'text', text: message }]
204
129
  };
205
130
  }
206
131
 
207
132
  const server = new McpServer(
208
- { name: "rag-mcp-bridge", version: "1.0.0" },
133
+ { name: 'rag-mcp-bridge', version: '1.0.0' },
209
134
  {
210
135
  instructions:
211
- "Use retrieve_knowledge before implementing requirement-sensitive code. " +
212
- "Every tool call requires an explicit platform: app, california-web, or shared. " +
213
- "add_prd_file and update_spec_file accept one file, an array of files, or a directory. " +
214
- "Only call update_spec_file after the PR is merged and leader review confirms the spec is effective.",
215
- },
136
+ 'Use retrieve_knowledge before implementing requirement-sensitive code. ' +
137
+ 'Every tool call requires an explicit platform: app, california-web, or shared. ' +
138
+ 'add_prd_file and update_spec_file accept one file, an array of files, or a directory. ' +
139
+ 'Only call update_spec_file after the PR is merged and leader review confirms the spec is effective.'
140
+ }
216
141
  );
217
142
 
218
143
  server.registerTool(
219
- "add_prd_file",
144
+ 'add_prd_file',
220
145
  {
221
- title: "录入或更新PRD基线",
146
+ title: '录入或更新PRD基线',
222
147
  description:
223
- "录入/更新PRD基线。paths支持单个md绝对路径、md绝对路径数组或文件夹绝对路径;文件夹会递归扫描全部.md文件。后端负责解析截图并将旧文档标记为作废。",
148
+ '录入/更新PRD基线。paths支持单个md绝对路径、md绝对路径数组或文件夹绝对路径;文件夹会递归扫描全部.md文件。后端负责解析截图并将旧文档标记为作废。',
224
149
  inputSchema: {
225
150
  paths: pathsSchema,
226
151
  platform: platformSchema,
227
152
  module: moduleSchema,
228
- page: pageSchema,
153
+ page: pageSchema
229
154
  },
230
155
  annotations: {
231
156
  readOnlyHint: false,
232
157
  destructiveHint: false,
233
158
  idempotentHint: false,
234
- openWorldHint: false,
235
- },
159
+ openWorldHint: false
160
+ }
236
161
  },
237
162
  async ({ paths, platform, module, page }) => {
238
163
  try {
239
164
  const expandedPaths = await expandMarkdownPaths(paths);
165
+ const documents = await buildDocuments(expandedPaths);
240
166
  return toolSuccess(
241
- await postJson("/add-prd", {
242
- paths: expandedPaths,
167
+ await postJson('/add-prd', {
168
+ documents,
243
169
  platform,
244
170
  module,
245
- page,
246
- }),
171
+ page: documents.length === 1 ? page : undefined
172
+ })
247
173
  );
248
174
  } catch (error) {
249
175
  return toolError(error);
250
176
  }
251
- },
177
+ }
252
178
  );
253
179
 
254
180
  server.registerTool(
255
- "update_spec_file",
181
+ 'update_spec_file',
256
182
  {
257
- title: "录入迭代变更Spec",
183
+ title: '录入迭代变更Spec',
258
184
  description:
259
- "仅在PR合并且leader review确认生效后调用。paths支持单个spec.md绝对路径、路径数组或文件夹;只需传改动spec。sprintVersion必须递增,数值越大需求越新。",
185
+ '仅在PR合并且leader review确认生效后调用。paths支持单个spec.md绝对路径、路径数组或文件夹;只需传改动spec。sprintVersion必须递增,数值越大需求越新。',
260
186
  inputSchema: {
261
187
  paths: pathsSchema,
262
188
  sprintVersion: z.number().int().positive(),
263
189
  iterationTag: z.string().min(1),
264
190
  platform: platformSchema,
265
191
  module: moduleSchema,
266
- page: pageSchema,
192
+ page: pageSchema
267
193
  },
268
194
  annotations: {
269
195
  readOnlyHint: false,
270
196
  destructiveHint: false,
271
197
  idempotentHint: false,
272
- openWorldHint: false,
273
- },
198
+ openWorldHint: false
199
+ }
274
200
  },
275
201
  async ({ paths, sprintVersion, iterationTag, platform, module, page }) => {
276
202
  try {
277
203
  const expandedPaths = await expandMarkdownPaths(paths);
204
+ const documents = await buildDocuments(expandedPaths);
278
205
  return toolSuccess(
279
- await postJson("/add-spec", {
280
- paths: expandedPaths,
206
+ await postJson('/add-spec', {
207
+ documents,
281
208
  sprintVersion,
282
209
  iterationTag,
283
210
  platform,
284
211
  module,
285
- page,
286
- }),
212
+ page: documents.length === 1 ? page : undefined
213
+ })
287
214
  );
288
215
  } catch (error) {
289
216
  return toolError(error);
290
217
  }
291
- },
218
+ }
292
219
  );
293
220
 
294
221
  server.registerTool(
295
- "retrieve_knowledge",
222
+ 'retrieve_knowledge',
296
223
  {
297
- title: "检索PRD与迭代Spec",
224
+ title: '检索PRD与迭代Spec',
298
225
  description:
299
- "Codex编写需求相关代码前调用。后端执行向量+BM25-RRF混合检索并按sprint_version业务排序。enableStruct=true时才生成结构化PRD;includeImages=true时才保留图片并替换为数据库图片URL,两者默认均为false。",
226
+ 'Codex编写需求相关代码前调用。后端执行向量+BM25-RRF混合检索并按sprint_version业务排序。enableStruct=true时才生成结构化PRD;includeImages=true时才保留图片并替换为数据库图片URL,两者默认均为false。',
300
227
  inputSchema: {
301
228
  query: z.string().min(1),
302
229
  platform: platformSchema,
303
- module: moduleSchema.describe(
304
- "可选业务模块硬过滤,例如vehicle-selector",
305
- ),
306
- page: pageSchema.describe(
307
- "可选页面硬过滤,例如首页传home;不传时后端会从明确的查询词自动推断",
308
- ),
230
+ module: moduleSchema.describe('可选业务模块硬过滤,例如vehicle-selector'),
231
+ page: pageSchema.describe('可选页面硬过滤,例如首页传home;不传时后端会从明确的查询词自动推断'),
309
232
  docKey: docKeySchema,
310
- enableStruct: z
311
- .boolean()
312
- .default(false)
313
- .describe("是否调用LLM生成structuredPrd,默认false"),
314
- includeImages: z
315
- .boolean()
316
- .default(false)
317
- .describe("是否返回可供UI显示的数据库图片URL,默认false"),
233
+ enableStruct: z.boolean().default(false).describe('是否调用LLM生成structuredPrd,默认false'),
234
+ includeImages: z.boolean().default(false).describe('是否返回可供UI显示的数据库图片URL,默认false')
318
235
  },
319
236
  annotations: {
320
237
  readOnlyHint: true,
321
238
  destructiveHint: false,
322
239
  idempotentHint: true,
323
- openWorldHint: false,
324
- },
240
+ openWorldHint: false
241
+ }
325
242
  },
326
- async ({
327
- query,
328
- platform,
329
- module,
330
- page,
331
- docKey,
332
- enableStruct = false,
333
- includeImages = false,
334
- }) => {
243
+ async ({ query, platform, module, page, docKey, enableStruct = false, includeImages = false }) => {
335
244
  try {
336
245
  return toolSuccess(
337
- await getJson("/search", {
246
+ await getJson('/search', {
338
247
  query: query.trim(),
339
248
  platform,
340
249
  module,
341
250
  page,
342
251
  docKey,
343
252
  enableStruct: String(enableStruct === true),
344
- includeImages: String(includeImages === true),
345
- }),
253
+ includeImages: String(includeImages === true)
254
+ })
346
255
  );
347
256
  } catch (error) {
348
257
  return toolError(error);
349
258
  }
350
- },
259
+ }
351
260
  );
352
261
 
353
262
  async function main() {
@@ -355,7 +264,7 @@ async function main() {
355
264
  await server.connect(transport);
356
265
  }
357
266
 
358
- main().catch((error) => {
359
- console.error("Failed to start rag-mcp-bridge:", error);
267
+ main().catch(error => {
268
+ console.error('Failed to start rag-mcp-bridge:', error);
360
269
  process.exitCode = 1;
361
270
  });