@autobest-ui/agent 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/README.md +182 -0
- package/bin/sync-assets.mjs +126 -0
- package/bin/sync-assets.test.mjs +64 -0
- package/mcp/azurepr-mcp-bridge/README.md +37 -0
- package/mcp/azurepr-mcp-bridge/azure-devops.js +327 -0
- package/mcp/azurepr-mcp-bridge/config.toml.example +7 -0
- package/mcp/azurepr-mcp-bridge/index.js +65 -0
- package/mcp/azurepr-mcp-bridge/index.test.js +116 -0
- package/mcp/azurepr-mcp-bridge/package.json +22 -0
- package/mcp/rag-mcp-bridge/README.md +42 -0
- package/mcp/rag-mcp-bridge/codex-system-prompt.md +20 -0
- package/mcp/rag-mcp-bridge/config.toml.example +12 -0
- package/mcp/rag-mcp-bridge/index.js +361 -0
- package/mcp/rag-mcp-bridge/index.test.js +56 -0
- package/mcp/rag-mcp-bridge/package.json +21 -0
- package/package.json +44 -0
- package/plugins/autobest-delivery/.codex-plugin/plugin.json +25 -0
- package/plugins/autobest-delivery/.mcp.json +11 -0
- package/plugins/autobest-delivery/README.md +164 -0
- package/plugins/autobest-delivery/assets/delivery-report-template.xlsx +0 -0
- package/plugins/autobest-delivery/mcp-server/npm-shrinkwrap.json +3511 -0
- package/plugins/autobest-delivery/mcp-server/package.json +23 -0
- package/plugins/autobest-delivery/mcp-server/src/paths.mjs +43 -0
- package/plugins/autobest-delivery/mcp-server/src/report.mjs +605 -0
- package/plugins/autobest-delivery/mcp-server/src/runner.mjs +489 -0
- package/plugins/autobest-delivery/mcp-server/src/server.mjs +199 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixture-server.mjs +36 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixtures/basic.feature.mjs +68 -0
- package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +83 -0
- package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +254 -0
- package/plugins/autobest-delivery/mcp-server/tests/runner.test.mjs +354 -0
- package/plugins/autobest-delivery/scripts/export-delivery-report.mjs +41 -0
- package/plugins/autobest-delivery/scripts/setup.mjs +295 -0
- package/plugins/autobest-delivery/scripts/setup.test.mjs +145 -0
- package/plugins/autobest-delivery/scripts/start-mcp.mjs +7 -0
- package/plugins/autobest-delivery/skills/code-audit/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/code-audit/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/code-craft/SKILL.md +27 -0
- package/plugins/autobest-delivery/skills/code-craft/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/SKILL.md +43 -0
- package/plugins/autobest-delivery/skills/delivery-loop/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +235 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/SKILL.md +35 -0
- package/plugins/autobest-delivery/skills/e2e-gen-spec/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/SKILL.md +30 -0
- package/plugins/autobest-delivery/skills/e2e-ui-checker/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/export-report/SKILL.md +66 -0
- package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +8 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/SKILL.md +24 -0
- package/plugins/autobest-delivery/skills/ui-structure-guard/agents/openai.yaml +7 -0
- package/skills/README.md +38 -0
- package/skills/common/figma-ui-capture/SKILL.md +197 -0
- package/skills/common/figma-ui-capture/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/SKILL.md +67 -0
- package/skills/common/ui-prd-scope/agents/openai.yaml +4 -0
- package/skills/common/ui-prd-scope/references/scope-schema.md +158 -0
- package/skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs +302 -0
- package/skills/react/react-code-standards/SKILL.md +78 -0
- package/skills/react/react-code-standards/agents/openai.yaml +4 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { diffLines } from "diff";
|
|
2
|
+
|
|
3
|
+
export const MAX_CONTENT_LENGTH = 120_000;
|
|
4
|
+
const MAX_FILE_CONTENT_LENGTH = 500_000;
|
|
5
|
+
const MAX_ADDED_CONTENT_LENGTH = 20_000;
|
|
6
|
+
const MAX_CHANGED_FILES = 200;
|
|
7
|
+
const MAX_REVIEW_COMMENTS = 500;
|
|
8
|
+
const MAX_COMMENT_LENGTH = 10_000;
|
|
9
|
+
const HTTP_TIMEOUT_MS = 30_000;
|
|
10
|
+
const FILE_FETCH_CONCURRENCY = 6;
|
|
11
|
+
|
|
12
|
+
const ignoredBinaryExtensions = new Set([
|
|
13
|
+
".png",
|
|
14
|
+
".jpg",
|
|
15
|
+
".jpeg",
|
|
16
|
+
".gif",
|
|
17
|
+
".webp",
|
|
18
|
+
".ico",
|
|
19
|
+
".pdf",
|
|
20
|
+
".zip",
|
|
21
|
+
".woff",
|
|
22
|
+
".woff2",
|
|
23
|
+
".ttf",
|
|
24
|
+
".eot",
|
|
25
|
+
".mp3",
|
|
26
|
+
".mp4",
|
|
27
|
+
".mov",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function isAttachmentPath(filePath) {
|
|
31
|
+
return (
|
|
32
|
+
typeof filePath === "string" &&
|
|
33
|
+
/(^|\/)\.attachments(?:\/|$)/i.test(filePath)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isBinaryPath(filePath) {
|
|
38
|
+
const extension = filePath?.toLowerCase().match(/\.[^/.]+$/)?.[0];
|
|
39
|
+
return extension ? ignoredBinaryExtensions.has(extension) : false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isInsideRequestedPath(filePath, requestedPath) {
|
|
43
|
+
if (typeof filePath !== "string" || !filePath) return false;
|
|
44
|
+
if (!requestedPath || requestedPath === "/") return true;
|
|
45
|
+
|
|
46
|
+
const normalized = `/${requestedPath}`
|
|
47
|
+
.replace(/\/{2,}/g, "/")
|
|
48
|
+
.replace(/\/$/, "");
|
|
49
|
+
return filePath === normalized || filePath.startsWith(`${normalized}/`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodePathSegment(value) {
|
|
53
|
+
try {
|
|
54
|
+
return decodeURIComponent(value);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error("Azure DevOps PR 地址包含无效编码");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parsePullRequestLocation(urlValue) {
|
|
61
|
+
const url = new URL(urlValue);
|
|
62
|
+
if (url.protocol !== "https:") {
|
|
63
|
+
throw new Error("Azure DevOps PR 地址必须使用 HTTPS");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hostname = url.hostname.toLowerCase();
|
|
67
|
+
const isVisualStudio = hostname.endsWith(".visualstudio.com");
|
|
68
|
+
const isDevAzure = hostname === "dev.azure.com";
|
|
69
|
+
if (!isVisualStudio && !isDevAzure) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"仅支持 dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const match = url.pathname.match(
|
|
76
|
+
isVisualStudio
|
|
77
|
+
? /^\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
|
|
78
|
+
: /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i,
|
|
79
|
+
);
|
|
80
|
+
if (!match) throw new Error("无法识别 Azure DevOps PR 地址");
|
|
81
|
+
|
|
82
|
+
if (isVisualStudio) {
|
|
83
|
+
const project = decodePathSegment(match[1]);
|
|
84
|
+
return {
|
|
85
|
+
apiRoot: `${url.origin}/${encodeURIComponent(project)}`,
|
|
86
|
+
repository: decodePathSegment(match[2]),
|
|
87
|
+
pullRequestId: match[3],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const organization = decodePathSegment(match[1]);
|
|
92
|
+
const project = decodePathSegment(match[2]);
|
|
93
|
+
return {
|
|
94
|
+
apiRoot: `${url.origin}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}`,
|
|
95
|
+
repository: decodePathSegment(match[3]),
|
|
96
|
+
pullRequestId: match[4],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function azureGet(url, headers, optional = false) {
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
headers,
|
|
103
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
if (optional) return undefined;
|
|
107
|
+
throw new Error(`Azure DevOps 获取失败:HTTP ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
return response.json();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function azureItemContent(repositoryRoot, filePath, commitId, headers) {
|
|
113
|
+
if (!commitId || !filePath || isBinaryPath(filePath)) return undefined;
|
|
114
|
+
|
|
115
|
+
const query = new URLSearchParams({
|
|
116
|
+
path: filePath,
|
|
117
|
+
includeContent: "true",
|
|
118
|
+
"versionDescriptor.version": commitId,
|
|
119
|
+
"versionDescriptor.versionType": "commit",
|
|
120
|
+
"api-version": "7.1",
|
|
121
|
+
});
|
|
122
|
+
const item = await azureGet(
|
|
123
|
+
`${repositoryRoot}/items?${query.toString()}`,
|
|
124
|
+
headers,
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
if (!item || item.isBinary || typeof item.content !== "string") {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (item.content.length > MAX_FILE_CONTENT_LENGTH) return undefined;
|
|
131
|
+
return item.content;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function extractAddedContent(previousContent, currentContent) {
|
|
135
|
+
if (typeof currentContent !== "string") return undefined;
|
|
136
|
+
|
|
137
|
+
const additions =
|
|
138
|
+
typeof previousContent === "string"
|
|
139
|
+
? diffLines(previousContent, currentContent)
|
|
140
|
+
.filter((part) => part.added)
|
|
141
|
+
.map((part) => part.value)
|
|
142
|
+
.join("")
|
|
143
|
+
: currentContent;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
content: additions.slice(0, MAX_ADDED_CONTENT_LENGTH),
|
|
147
|
+
truncated: additions.length > MAX_ADDED_CONTENT_LENGTH,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
152
|
+
const results = new Array(items.length);
|
|
153
|
+
let nextIndex = 0;
|
|
154
|
+
|
|
155
|
+
async function worker() {
|
|
156
|
+
while (nextIndex < items.length) {
|
|
157
|
+
const index = nextIndex++;
|
|
158
|
+
results[index] = await mapper(items[index]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await Promise.all(
|
|
163
|
+
Array.from(
|
|
164
|
+
{ length: Math.min(concurrency, items.length) },
|
|
165
|
+
() => worker(),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
return results;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function truncateText(value, maximumLength) {
|
|
172
|
+
if (typeof value !== "string") return value;
|
|
173
|
+
return value.length > maximumLength ? value.slice(0, maximumLength) : value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function withoutAddedContent(file) {
|
|
177
|
+
const { addedContent: _addedContent, ...metadata } = file;
|
|
178
|
+
return metadata;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function serializePullRequest(result) {
|
|
182
|
+
const sourceFiles = result.changedFiles;
|
|
183
|
+
const bounded = {
|
|
184
|
+
...result,
|
|
185
|
+
changedFiles: sourceFiles.map(withoutAddedContent),
|
|
186
|
+
addedContentOmittedDueToResponseLimit: 0,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let omittedComments = 0;
|
|
190
|
+
let omittedFiles = 0;
|
|
191
|
+
let serialized = JSON.stringify(bounded, null, 2);
|
|
192
|
+
|
|
193
|
+
while (
|
|
194
|
+
serialized.length > MAX_CONTENT_LENGTH &&
|
|
195
|
+
bounded.reviewComments.length > 0
|
|
196
|
+
) {
|
|
197
|
+
bounded.reviewComments.pop();
|
|
198
|
+
omittedComments += 1;
|
|
199
|
+
serialized = JSON.stringify(bounded, null, 2);
|
|
200
|
+
}
|
|
201
|
+
while (
|
|
202
|
+
serialized.length > MAX_CONTENT_LENGTH &&
|
|
203
|
+
bounded.changedFiles.length > 0
|
|
204
|
+
) {
|
|
205
|
+
bounded.changedFiles.pop();
|
|
206
|
+
omittedFiles += 1;
|
|
207
|
+
serialized = JSON.stringify(bounded, null, 2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (omittedComments > 0) bounded.omittedReviewCommentCount = omittedComments;
|
|
211
|
+
if (omittedFiles > 0) bounded.omittedChangedFileCount = omittedFiles;
|
|
212
|
+
if (omittedComments > 0 || omittedFiles > 0) bounded.responseTruncated = true;
|
|
213
|
+
serialized = JSON.stringify(bounded, null, 2);
|
|
214
|
+
|
|
215
|
+
for (let index = 0; index < bounded.changedFiles.length; index += 1) {
|
|
216
|
+
const addedContent = sourceFiles[index].addedContent;
|
|
217
|
+
if (!addedContent) continue;
|
|
218
|
+
|
|
219
|
+
bounded.changedFiles[index].addedContent = addedContent;
|
|
220
|
+
const candidate = JSON.stringify(bounded, null, 2);
|
|
221
|
+
if (candidate.length <= MAX_CONTENT_LENGTH) {
|
|
222
|
+
serialized = candidate;
|
|
223
|
+
} else {
|
|
224
|
+
delete bounded.changedFiles[index].addedContent;
|
|
225
|
+
bounded.addedContentOmittedDueToResponseLimit += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
serialized = JSON.stringify(bounded, null, 2);
|
|
230
|
+
if (serialized.length <= MAX_CONTENT_LENGTH) return serialized;
|
|
231
|
+
|
|
232
|
+
return JSON.stringify({
|
|
233
|
+
title: truncateText(result.title, 500),
|
|
234
|
+
status: result.status,
|
|
235
|
+
responseTruncated: true,
|
|
236
|
+
error: "PR 响应超过 MCP 内容上限",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function getAzurePullRequest(urlValue, pat) {
|
|
241
|
+
const inputUrl = new URL(urlValue);
|
|
242
|
+
const requestedPath = inputUrl.searchParams.get("path");
|
|
243
|
+
const { apiRoot, repository, pullRequestId } =
|
|
244
|
+
parsePullRequestLocation(urlValue);
|
|
245
|
+
const encodedRepository = encodeURIComponent(repository);
|
|
246
|
+
const repositoryRoot = `${apiRoot}/_apis/git/repositories/${encodedRepository}`;
|
|
247
|
+
const root = `${repositoryRoot}/pullRequests/${pullRequestId}`;
|
|
248
|
+
const headers = {
|
|
249
|
+
Accept: "application/json",
|
|
250
|
+
Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}`,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const [pullRequest, threads, workItems, iterations] = await Promise.all([
|
|
254
|
+
azureGet(`${root}?api-version=7.1`, headers),
|
|
255
|
+
azureGet(`${root}/threads?api-version=7.1`, headers, true),
|
|
256
|
+
azureGet(`${root}/workitems?api-version=7.1`, headers, true),
|
|
257
|
+
azureGet(`${root}/iterations?api-version=7.1`, headers, true),
|
|
258
|
+
]);
|
|
259
|
+
const latestIterationId = Math.max(
|
|
260
|
+
0,
|
|
261
|
+
...(iterations?.value || []).map(({ id }) => Number(id) || 0),
|
|
262
|
+
);
|
|
263
|
+
const changes = latestIterationId
|
|
264
|
+
? await azureGet(
|
|
265
|
+
`${root}/iterations/${latestIterationId}/changes?$top=${MAX_CHANGED_FILES}&api-version=7.1`,
|
|
266
|
+
headers,
|
|
267
|
+
true,
|
|
268
|
+
)
|
|
269
|
+
: undefined;
|
|
270
|
+
|
|
271
|
+
const sourceCommitId = pullRequest.lastMergeSourceCommit?.commitId;
|
|
272
|
+
const targetCommitId = pullRequest.lastMergeTargetCommit?.commitId;
|
|
273
|
+
const changeEntries = (changes?.changeEntries || [])
|
|
274
|
+
.filter(({ item }) => !isAttachmentPath(item?.path))
|
|
275
|
+
.filter(({ item }) => isInsideRequestedPath(item?.path, requestedPath))
|
|
276
|
+
.slice(0, MAX_CHANGED_FILES);
|
|
277
|
+
const changedFiles = await mapWithConcurrency(
|
|
278
|
+
changeEntries,
|
|
279
|
+
FILE_FETCH_CONCURRENCY,
|
|
280
|
+
async ({ changeType, item }) => {
|
|
281
|
+
const filePath = item?.path;
|
|
282
|
+
const deleted = String(changeType).toLowerCase().includes("delete");
|
|
283
|
+
const isFile = !item?.gitObjectType || item.gitObjectType === "blob";
|
|
284
|
+
if (!filePath || deleted || !isFile || isBinaryPath(filePath)) {
|
|
285
|
+
return { changeType, path: filePath };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const [previousContent, currentContent] = await Promise.all([
|
|
289
|
+
azureItemContent(repositoryRoot, filePath, targetCommitId, headers),
|
|
290
|
+
azureItemContent(repositoryRoot, filePath, sourceCommitId, headers),
|
|
291
|
+
]);
|
|
292
|
+
const additions = extractAddedContent(previousContent, currentContent);
|
|
293
|
+
return {
|
|
294
|
+
changeType,
|
|
295
|
+
path: filePath,
|
|
296
|
+
addedContent: additions?.content || undefined,
|
|
297
|
+
addedContentTruncated: additions?.truncated || undefined,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
title: truncateText(pullRequest.title, 2_000),
|
|
304
|
+
description: truncateText(pullRequest.description, 20_000),
|
|
305
|
+
status: pullRequest.status,
|
|
306
|
+
sourceBranch: pullRequest.sourceRefName,
|
|
307
|
+
targetBranch: pullRequest.targetRefName,
|
|
308
|
+
reviewers: (pullRequest.reviewers || []).map(({ displayName, vote }) => ({
|
|
309
|
+
displayName: truncateText(displayName, 1_000),
|
|
310
|
+
vote,
|
|
311
|
+
})),
|
|
312
|
+
reviewComments: (threads?.value || [])
|
|
313
|
+
.flatMap((thread) =>
|
|
314
|
+
(thread.comments || [])
|
|
315
|
+
.filter((comment) => !comment.isDeleted && comment.content)
|
|
316
|
+
.map(({ content, author }) => ({
|
|
317
|
+
content: truncateText(content, MAX_COMMENT_LENGTH),
|
|
318
|
+
author: truncateText(author?.displayName, 1_000),
|
|
319
|
+
})),
|
|
320
|
+
)
|
|
321
|
+
.slice(0, MAX_REVIEW_COMMENTS),
|
|
322
|
+
workItems: (workItems?.value || []).map(({ id, url }) => ({ id, url })),
|
|
323
|
+
requestedPath: requestedPath || undefined,
|
|
324
|
+
latestIterationId,
|
|
325
|
+
changedFiles,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { getAzurePullRequest, serializePullRequest } from "./azure-devops.js";
|
|
7
|
+
|
|
8
|
+
const server = new McpServer(
|
|
9
|
+
{ name: "azurepr-mcp-bridge", version: "1.0.0" },
|
|
10
|
+
{
|
|
11
|
+
instructions:
|
|
12
|
+
"Use get_azure_pull_request to read Azure DevOps pull request metadata, review comments, linked work items, and changed text files. " +
|
|
13
|
+
"The tool is read-only and requires an HTTPS PR URL from dev.azure.com or *.visualstudio.com. " +
|
|
14
|
+
"AZURE_DEVOPS_PAT is used only for Azure DevOps API authentication and must never be exposed in output.",
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
server.registerTool(
|
|
19
|
+
"get_azure_pull_request",
|
|
20
|
+
{
|
|
21
|
+
title: "读取 Azure DevOps PR",
|
|
22
|
+
description:
|
|
23
|
+
"读取 PR 基本信息、评审意见、关联工作项和变更文件。URL 可使用 path 查询参数限定仓库目录,例如 ?path=/src。",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
url: z
|
|
26
|
+
.url()
|
|
27
|
+
.describe("dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址"),
|
|
28
|
+
},
|
|
29
|
+
annotations: {
|
|
30
|
+
readOnlyHint: true,
|
|
31
|
+
destructiveHint: false,
|
|
32
|
+
idempotentHint: true,
|
|
33
|
+
openWorldHint: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async ({ url }) => {
|
|
37
|
+
try {
|
|
38
|
+
const pat = process.env.AZURE_DEVOPS_PAT?.trim();
|
|
39
|
+
if (!pat) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"AZURE_DEVOPS_PAT 未配置,Stdio MCP 无法读取 Azure DevOps PR",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const result = await getAzurePullRequest(url, pat);
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: serializePullRequest(result) }],
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return {
|
|
51
|
+
isError: true,
|
|
52
|
+
content: [{ type: "text", text: message }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
await server.connect(new StdioServerTransport());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((error) => {
|
|
63
|
+
console.error("Failed to start azurepr-mcp-bridge:", error);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
extractAddedContent,
|
|
9
|
+
isAttachmentPath,
|
|
10
|
+
isBinaryPath,
|
|
11
|
+
isInsideRequestedPath,
|
|
12
|
+
MAX_CONTENT_LENGTH,
|
|
13
|
+
parsePullRequestLocation,
|
|
14
|
+
serializePullRequest,
|
|
15
|
+
} from "./azure-devops.js";
|
|
16
|
+
|
|
17
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
test("parses supported Azure DevOps PR URLs", () => {
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
parsePullRequestLocation(
|
|
22
|
+
"https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
|
|
23
|
+
),
|
|
24
|
+
{
|
|
25
|
+
apiRoot: "https://autobest.visualstudio.com/AutoBestChina",
|
|
26
|
+
repository: "web",
|
|
27
|
+
pullRequestId: "42",
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
parsePullRequestLocation(
|
|
32
|
+
"https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7",
|
|
33
|
+
),
|
|
34
|
+
{
|
|
35
|
+
apiRoot: "https://dev.azure.com/example/My%20Project",
|
|
36
|
+
repository: "front end",
|
|
37
|
+
pullRequestId: "7",
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
assert.throws(
|
|
41
|
+
() =>
|
|
42
|
+
parsePullRequestLocation(
|
|
43
|
+
"https://example.com/org/project/_git/repo/pullrequest/1",
|
|
44
|
+
),
|
|
45
|
+
/仅支持/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("filters paths and extracts added lines", () => {
|
|
50
|
+
assert.equal(isAttachmentPath("/docs/.attachments/image.png"), true);
|
|
51
|
+
assert.equal(isBinaryPath("/assets/logo.PNG"), true);
|
|
52
|
+
assert.equal(isInsideRequestedPath("/src/app/index.js", "/src"), true);
|
|
53
|
+
assert.equal(isInsideRequestedPath("/scripts/build.js", "/src"), false);
|
|
54
|
+
assert.deepEqual(extractAddedContent("a\nb\n", "a\nc\nb\n"), {
|
|
55
|
+
content: "c\n",
|
|
56
|
+
truncated: false,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("serializes oversized results as valid bounded JSON", () => {
|
|
61
|
+
const text = serializePullRequest({
|
|
62
|
+
title: "Large PR",
|
|
63
|
+
description: "description",
|
|
64
|
+
status: "active",
|
|
65
|
+
reviewers: [],
|
|
66
|
+
reviewComments: [],
|
|
67
|
+
workItems: [],
|
|
68
|
+
latestIterationId: 1,
|
|
69
|
+
changedFiles: Array.from({ length: 20 }, (_, index) => ({
|
|
70
|
+
changeType: "edit",
|
|
71
|
+
path: `/src/file-${index}.js`,
|
|
72
|
+
addedContent: "x".repeat(20_000),
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.ok(text.length <= MAX_CONTENT_LENGTH);
|
|
77
|
+
assert.doesNotThrow(() => JSON.parse(text));
|
|
78
|
+
assert.ok(JSON.parse(text).addedContentOmittedDueToResponseLimit > 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("initializes and exposes the Azure PR tool over stdio", async () => {
|
|
82
|
+
const client = new Client({
|
|
83
|
+
name: "azurepr-mcp-bridge-test",
|
|
84
|
+
version: "1.0.0",
|
|
85
|
+
});
|
|
86
|
+
const transport = new StdioClientTransport({
|
|
87
|
+
command: process.execPath,
|
|
88
|
+
args: [path.join(directory, "index.js")],
|
|
89
|
+
cwd: path.resolve(directory, "../.."),
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await client.connect(transport);
|
|
95
|
+
assert.equal(client.getServerVersion()?.name, "azurepr-mcp-bridge");
|
|
96
|
+
assert.match(client.getInstructions() ?? "", /AZURE_DEVOPS_PAT/);
|
|
97
|
+
|
|
98
|
+
const { tools } = await client.listTools();
|
|
99
|
+
assert.deepEqual(tools.map(({ name }) => name), [
|
|
100
|
+
"get_azure_pull_request",
|
|
101
|
+
]);
|
|
102
|
+
assert.equal(tools[0].annotations?.readOnlyHint, true);
|
|
103
|
+
assert.equal(tools[0].annotations?.openWorldHint, true);
|
|
104
|
+
|
|
105
|
+
const missingPatResult = await client.callTool({
|
|
106
|
+
name: "get_azure_pull_request",
|
|
107
|
+
arguments: {
|
|
108
|
+
url: "https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
assert.equal(missingPatResult.isError, true);
|
|
112
|
+
assert.match(missingPatResult.content[0].text, /AZURE_DEVOPS_PAT 未配置/);
|
|
113
|
+
} finally {
|
|
114
|
+
await client.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autobest/azurepr-mcp-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Read-only Azure DevOps pull request MCP server",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node index.js",
|
|
13
|
+
"lint": "node --check index.js && node --check azure-devops.js && node --check index.test.js",
|
|
14
|
+
"test": "node --test index.test.js",
|
|
15
|
+
"verify": "npm run lint && npm test"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
19
|
+
"diff": "9.0.0",
|
|
20
|
+
"zod": "4.4.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# rag-mcp-bridge
|
|
2
|
+
|
|
3
|
+
`rag-mcp-bridge` 是 PRD Knowledge RAG API 的本地 STDIO MCP 适配器。它负责展开 Markdown 路径并把 MCP 工具调用转发给 HTTP API;文档解析、Embedding、数据库读写和检索仍由 Express 服务完成。
|
|
4
|
+
|
|
5
|
+
## 启动
|
|
6
|
+
|
|
7
|
+
先启动 PRD Knowledge HTTP API,并在 Codex 的 `config.toml` 中加入 [config.toml.example](config.toml.example) 的配置。Codex 将通过 npm 安装并启动发布包中的 MCP:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx --yes --package=@autobest-ui/agent@latest autobest-rag-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@latest` 可以替换为明确版本,例如 `@1.0.0`。STDIO 用于 MCP 协议通信,服务不会向标准输出写日志。
|
|
14
|
+
|
|
15
|
+
## 环境变量
|
|
16
|
+
|
|
17
|
+
| 变量 | 默认值 | 说明 |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `RAG_API_BASE_URL` | 无,必填 | PRD Knowledge HTTP API 根地址,必须通过 `config.toml` 的 `[mcp_servers.rag-mcp-bridge.env]` 配置 |
|
|
20
|
+
| `RAG_MCP_HTTP_TIMEOUT_MS` | `120000` | 单次 HTTP 请求超时,单位为毫秒 |
|
|
21
|
+
|
|
22
|
+
示例:
|
|
23
|
+
|
|
24
|
+
```toml
|
|
25
|
+
[mcp_servers.rag-mcp-bridge.env]
|
|
26
|
+
RAG_API_BASE_URL = "http://127.0.0.1:3000/api/knowledge"
|
|
27
|
+
RAG_MCP_HTTP_TIMEOUT_MS = "120000"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`RAG_API_BASE_URL` 仅在 MCP 客户端配置中提供,服务代码不包含默认 API 地址。完整配置见 [config.toml.example](config.toml.example)。
|
|
31
|
+
|
|
32
|
+
## 工具
|
|
33
|
+
|
|
34
|
+
- `add_prd_file`:录入或更新 PRD 基线。
|
|
35
|
+
- `update_spec_file`:录入已经合并并确认生效的迭代 Spec。
|
|
36
|
+
- `retrieve_knowledge`:检索 PRD 基线和迭代 Spec。
|
|
37
|
+
|
|
38
|
+
运行协议测试:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run test:mcp:rag
|
|
42
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
你可以通过 `rag-mcp-bridge` 获取和维护PRD知识库中的需求依据。
|
|
2
|
+
|
|
3
|
+
知识库规则:
|
|
4
|
+
1. `prd-module` 是PRD基础需求,`sprint_version=0`、`priority=10`。
|
|
5
|
+
2. `iteration-spec` 是迭代变更需求,`priority=1`。同一业务点存在多份spec时,必须以 `sprint_version` 最大的文档为实现依据;旧spec只能参考。
|
|
6
|
+
3. spec未覆盖的内容才允许使用PRD基线。
|
|
7
|
+
4. 编写或修改需求相关代码前,主动调用 `retrieve_knowledge`。普通检索不要传 `enableStruct` 和 `includeImages`,两者默认均为 `false`。
|
|
8
|
+
5. 完成代码后,输出实现依据,包括文档名称与 `sprint_version`。
|
|
9
|
+
6. 所有录入和检索必须显式指定 `platform`:APP使用 `app`,加州网站使用 `california-web`,真正跨端共用的需求才使用 `shared`。
|
|
10
|
+
7. 查询 `app` 或 `california-web` 时允许后端补充 `shared`,但禁止跨查另一个平台;查询目标平台不明确时先向用户确认,不能靠语义猜测。
|
|
11
|
+
8. 迭代spec必须使用与其PRD基线相同的 `platform`;可选 `module` 用于进一步标识业务模块。
|
|
12
|
+
9. 页面是业务边界。查询明确提到首页、PL、PN、PD、All Category、Category、Subcategory、Name Search、Acc或其他页面时,调用 `retrieve_knowledge` 必须传对应 `page`,分别为 `home`、`pl`、`pn`、`pd`、`all-category`、`category`、`subcategory`、`name-search`、`acc`、`other`。
|
|
13
|
+
10. 已知目标文档绝对路径时优先传 `docKey` 做单文档硬过滤。不得仅依赖向量相似度区分不同页面。
|
|
14
|
+
|
|
15
|
+
工具使用时机:
|
|
16
|
+
- `add_prd_file`:首次录入或更新PRD基线时调用。必须传 `platform`;`paths` 支持单个md文件绝对路径、md文件绝对路径数组、文件夹绝对路径。文件夹会扫描其中全部md文件。后端默认按每个文件名推断 `page`;只有录入单个文件且需要纠正推断时才显式传 `page`。
|
|
17
|
+
- `update_spec_file`:仅在PR已经合并且leader review确认变更生效后调用。必须传与基线一致的 `platform`;`paths` 同样支持单个spec文件、spec文件数组、spec文件夹;只传本次改动spec,不需要完整PRD。`sprintVersion` 必须按迭代递增。单文件spec可以显式传其 `page`。
|
|
18
|
+
- `retrieve_knowledge`:分析需求、制定实现方案、编写代码或判断历史行为前调用。必须根据目标端传 `platform`,已知模块时传 `module`,明确页面时传 `page`,已知文档时传 `docKey`。仅明确需要六字段结构化PRD时传 `enableStruct=true`;仅需要在UI显示需求截图时传 `includeImages=true`。
|
|
19
|
+
|
|
20
|
+
`rag-mcp-bridge` 仅执行文件夹扫描和HTTP转发。文档解析、截图提取、Embedding、RAG与PostgreSQL操作全部由Express后端完成。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[mcp_servers.rag-mcp-bridge]
|
|
2
|
+
command = "npx"
|
|
3
|
+
args = ["--yes", "--package=@autobest-ui/agent@latest", "autobest-rag-mcp"]
|
|
4
|
+
startup_timeout_sec = 30
|
|
5
|
+
tool_timeout_sec = 120
|
|
6
|
+
enabled = true
|
|
7
|
+
|
|
8
|
+
[mcp_servers.rag-mcp-bridge.env]
|
|
9
|
+
# 必填:按实际部署地址修改,MCP 服务本身不提供默认地址。
|
|
10
|
+
RAG_API_BASE_URL = "http://127.0.0.1:3000/api/knowledge"
|
|
11
|
+
# 可选:单次 HTTP 请求超时,单位为毫秒。
|
|
12
|
+
RAG_MCP_HTTP_TIMEOUT_MS = "120000"
|