@autobest-ui/agent 1.0.1 → 1.0.2
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 +3 -3
- package/mcp/azurepr-mcp-bridge/azure-devops.js +82 -112
- package/mcp/azurepr-mcp-bridge/index.js +21 -25
- package/mcp/azurepr-mcp-bridge/index.test.js +50 -59
- package/mcp/figma-mcp-bridge/src/tools/index.js +782 -361
- package/mcp/figma-mcp-bridge/src/tools/mutations.js +3566 -2160
- package/mcp/figma-mcp-bridge/src/tools/nodes.js +53 -32
- package/mcp/figma-mcp-bridge/src/tools/pages.js +38 -23
- package/mcp/rag-mcp-bridge/README.md +2 -2
- package/mcp/rag-mcp-bridge/config.toml.example +1 -1
- package/mcp/rag-mcp-bridge/index.js +80 -125
- package/mcp/rag-mcp-bridge/index.test.js +21 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npx --yes --package=@autobest-ui/agent@latest autobest-agent-sync react
|
|
|
25
25
|
先启动 PRD Knowledge HTTP API,再验证 RAG MCP:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
RAG_API_BASE_URL="http://
|
|
28
|
+
RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
|
|
29
29
|
npx --yes \
|
|
30
30
|
--package=@autobest-ui/agent@latest \
|
|
31
31
|
autobest-rag-mcp
|
|
@@ -159,7 +159,7 @@ Common Skills 会写入 `~/.agents/skills`,React Skills 会写入目标项目
|
|
|
159
159
|
RAG MCP:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
RAG_API_BASE_URL="http://
|
|
162
|
+
RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
|
|
163
163
|
npx --yes \
|
|
164
164
|
--package="$AGENT_TARBALL_PATH" \
|
|
165
165
|
autobest-rag-mcp
|
|
@@ -206,7 +206,7 @@ args = [
|
|
|
206
206
|
]
|
|
207
207
|
|
|
208
208
|
[mcp_servers.rag-mcp-bridge.env]
|
|
209
|
-
RAG_API_BASE_URL = "http://
|
|
209
|
+
RAG_API_BASE_URL = "http://192.168.1.12:3000/api/knowledge"
|
|
210
210
|
|
|
211
211
|
[mcp_servers.azurepr-mcp-bridge]
|
|
212
212
|
command = "npx"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { diffLines } from
|
|
1
|
+
import { diffLines } from 'diff';
|
|
2
2
|
|
|
3
3
|
export const MAX_CONTENT_LENGTH = 120_000;
|
|
4
4
|
const MAX_FILE_CONTENT_LENGTH = 500_000;
|
|
@@ -10,28 +10,25 @@ const HTTP_TIMEOUT_MS = 30_000;
|
|
|
10
10
|
const FILE_FETCH_CONCURRENCY = 6;
|
|
11
11
|
|
|
12
12
|
const ignoredBinaryExtensions = new Set([
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
28
|
]);
|
|
29
29
|
|
|
30
30
|
export function isAttachmentPath(filePath) {
|
|
31
|
-
return (
|
|
32
|
-
typeof filePath === "string" &&
|
|
33
|
-
/(^|\/)\.attachments(?:\/|$)/i.test(filePath)
|
|
34
|
-
);
|
|
31
|
+
return typeof filePath === 'string' && /(^|\/)\.attachments(?:\/|$)/i.test(filePath);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
export function isBinaryPath(filePath) {
|
|
@@ -40,12 +37,10 @@ export function isBinaryPath(filePath) {
|
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
export function isInsideRequestedPath(filePath, requestedPath) {
|
|
43
|
-
if (typeof filePath !==
|
|
44
|
-
if (!requestedPath || requestedPath ===
|
|
40
|
+
if (typeof filePath !== 'string' || !filePath) return false;
|
|
41
|
+
if (!requestedPath || requestedPath === '/') return true;
|
|
45
42
|
|
|
46
|
-
const normalized = `/${requestedPath}
|
|
47
|
-
.replace(/\/{2,}/g, "/")
|
|
48
|
-
.replace(/\/$/, "");
|
|
43
|
+
const normalized = `/${requestedPath}`.replace(/\/{2,}/g, '/').replace(/\/$/, '');
|
|
49
44
|
return filePath === normalized || filePath.startsWith(`${normalized}/`);
|
|
50
45
|
}
|
|
51
46
|
|
|
@@ -53,38 +48,36 @@ function decodePathSegment(value) {
|
|
|
53
48
|
try {
|
|
54
49
|
return decodeURIComponent(value);
|
|
55
50
|
} catch {
|
|
56
|
-
throw new Error(
|
|
51
|
+
throw new Error('Azure DevOps PR 地址包含无效编码');
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
export function parsePullRequestLocation(urlValue) {
|
|
61
56
|
const url = new URL(urlValue);
|
|
62
|
-
if (url.protocol !==
|
|
63
|
-
throw new Error(
|
|
57
|
+
if (url.protocol !== 'https:') {
|
|
58
|
+
throw new Error('Azure DevOps PR 地址必须使用 HTTPS');
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
const hostname = url.hostname.toLowerCase();
|
|
67
|
-
const isVisualStudio = hostname.endsWith(
|
|
68
|
-
const isDevAzure = hostname ===
|
|
62
|
+
const isVisualStudio = hostname.endsWith('.visualstudio.com');
|
|
63
|
+
const isDevAzure = hostname === 'dev.azure.com';
|
|
69
64
|
if (!isVisualStudio && !isDevAzure) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
"仅支持 dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址",
|
|
72
|
-
);
|
|
65
|
+
throw new Error('仅支持 dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址');
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
const match = url.pathname.match(
|
|
76
69
|
isVisualStudio
|
|
77
70
|
? /^\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
|
|
78
|
-
: /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
|
|
71
|
+
: /^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/(\d+)\/?$/i
|
|
79
72
|
);
|
|
80
|
-
if (!match) throw new Error(
|
|
73
|
+
if (!match) throw new Error('无法识别 Azure DevOps PR 地址');
|
|
81
74
|
|
|
82
75
|
if (isVisualStudio) {
|
|
83
76
|
const project = decodePathSegment(match[1]);
|
|
84
77
|
return {
|
|
85
78
|
apiRoot: `${url.origin}/${encodeURIComponent(project)}`,
|
|
86
79
|
repository: decodePathSegment(match[2]),
|
|
87
|
-
pullRequestId: match[3]
|
|
80
|
+
pullRequestId: match[3]
|
|
88
81
|
};
|
|
89
82
|
}
|
|
90
83
|
|
|
@@ -93,14 +86,14 @@ export function parsePullRequestLocation(urlValue) {
|
|
|
93
86
|
return {
|
|
94
87
|
apiRoot: `${url.origin}/${encodeURIComponent(organization)}/${encodeURIComponent(project)}`,
|
|
95
88
|
repository: decodePathSegment(match[3]),
|
|
96
|
-
pullRequestId: match[4]
|
|
89
|
+
pullRequestId: match[4]
|
|
97
90
|
};
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
async function azureGet(url, headers, optional = false) {
|
|
101
94
|
const response = await fetch(url, {
|
|
102
95
|
headers,
|
|
103
|
-
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
96
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
|
104
97
|
});
|
|
105
98
|
if (!response.ok) {
|
|
106
99
|
if (optional) return undefined;
|
|
@@ -114,17 +107,13 @@ async function azureItemContent(repositoryRoot, filePath, commitId, headers) {
|
|
|
114
107
|
|
|
115
108
|
const query = new URLSearchParams({
|
|
116
109
|
path: filePath,
|
|
117
|
-
includeContent:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
includeContent: 'true',
|
|
111
|
+
'versionDescriptor.version': commitId,
|
|
112
|
+
'versionDescriptor.versionType': 'commit',
|
|
113
|
+
'api-version': '7.1'
|
|
121
114
|
});
|
|
122
|
-
const item = await azureGet(
|
|
123
|
-
|
|
124
|
-
headers,
|
|
125
|
-
true,
|
|
126
|
-
);
|
|
127
|
-
if (!item || item.isBinary || typeof item.content !== "string") {
|
|
115
|
+
const item = await azureGet(`${repositoryRoot}/items?${query.toString()}`, headers, true);
|
|
116
|
+
if (!item || item.isBinary || typeof item.content !== 'string') {
|
|
128
117
|
return undefined;
|
|
129
118
|
}
|
|
130
119
|
if (item.content.length > MAX_FILE_CONTENT_LENGTH) return undefined;
|
|
@@ -132,19 +121,19 @@ async function azureItemContent(repositoryRoot, filePath, commitId, headers) {
|
|
|
132
121
|
}
|
|
133
122
|
|
|
134
123
|
export function extractAddedContent(previousContent, currentContent) {
|
|
135
|
-
if (typeof currentContent !==
|
|
124
|
+
if (typeof currentContent !== 'string') return undefined;
|
|
136
125
|
|
|
137
126
|
const additions =
|
|
138
|
-
typeof previousContent ===
|
|
127
|
+
typeof previousContent === 'string'
|
|
139
128
|
? diffLines(previousContent, currentContent)
|
|
140
|
-
.filter(
|
|
141
|
-
.map(
|
|
142
|
-
.join(
|
|
129
|
+
.filter(part => part.added)
|
|
130
|
+
.map(part => part.value)
|
|
131
|
+
.join('')
|
|
143
132
|
: currentContent;
|
|
144
133
|
|
|
145
134
|
return {
|
|
146
135
|
content: additions.slice(0, MAX_ADDED_CONTENT_LENGTH),
|
|
147
|
-
truncated: additions.length > MAX_ADDED_CONTENT_LENGTH
|
|
136
|
+
truncated: additions.length > MAX_ADDED_CONTENT_LENGTH
|
|
148
137
|
};
|
|
149
138
|
}
|
|
150
139
|
|
|
@@ -159,17 +148,12 @@ async function mapWithConcurrency(items, concurrency, mapper) {
|
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
|
|
162
|
-
await Promise.all(
|
|
163
|
-
Array.from(
|
|
164
|
-
{ length: Math.min(concurrency, items.length) },
|
|
165
|
-
() => worker(),
|
|
166
|
-
),
|
|
167
|
-
);
|
|
151
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
168
152
|
return results;
|
|
169
153
|
}
|
|
170
154
|
|
|
171
155
|
function truncateText(value, maximumLength) {
|
|
172
|
-
if (typeof value !==
|
|
156
|
+
if (typeof value !== 'string') return value;
|
|
173
157
|
return value.length > maximumLength ? value.slice(0, maximumLength) : value;
|
|
174
158
|
}
|
|
175
159
|
|
|
@@ -183,25 +167,19 @@ export function serializePullRequest(result) {
|
|
|
183
167
|
const bounded = {
|
|
184
168
|
...result,
|
|
185
169
|
changedFiles: sourceFiles.map(withoutAddedContent),
|
|
186
|
-
addedContentOmittedDueToResponseLimit: 0
|
|
170
|
+
addedContentOmittedDueToResponseLimit: 0
|
|
187
171
|
};
|
|
188
172
|
|
|
189
173
|
let omittedComments = 0;
|
|
190
174
|
let omittedFiles = 0;
|
|
191
175
|
let serialized = JSON.stringify(bounded, null, 2);
|
|
192
176
|
|
|
193
|
-
while (
|
|
194
|
-
serialized.length > MAX_CONTENT_LENGTH &&
|
|
195
|
-
bounded.reviewComments.length > 0
|
|
196
|
-
) {
|
|
177
|
+
while (serialized.length > MAX_CONTENT_LENGTH && bounded.reviewComments.length > 0) {
|
|
197
178
|
bounded.reviewComments.pop();
|
|
198
179
|
omittedComments += 1;
|
|
199
180
|
serialized = JSON.stringify(bounded, null, 2);
|
|
200
181
|
}
|
|
201
|
-
while (
|
|
202
|
-
serialized.length > MAX_CONTENT_LENGTH &&
|
|
203
|
-
bounded.changedFiles.length > 0
|
|
204
|
-
) {
|
|
182
|
+
while (serialized.length > MAX_CONTENT_LENGTH && bounded.changedFiles.length > 0) {
|
|
205
183
|
bounded.changedFiles.pop();
|
|
206
184
|
omittedFiles += 1;
|
|
207
185
|
serialized = JSON.stringify(bounded, null, 2);
|
|
@@ -233,38 +211,34 @@ export function serializePullRequest(result) {
|
|
|
233
211
|
title: truncateText(result.title, 500),
|
|
234
212
|
status: result.status,
|
|
235
213
|
responseTruncated: true,
|
|
236
|
-
error:
|
|
214
|
+
error: 'PR 响应超过 MCP 内容上限'
|
|
237
215
|
});
|
|
238
216
|
}
|
|
239
217
|
|
|
240
218
|
export async function getAzurePullRequest(urlValue, pat) {
|
|
241
219
|
const inputUrl = new URL(urlValue);
|
|
242
|
-
const requestedPath = inputUrl.searchParams.get(
|
|
243
|
-
const { apiRoot, repository, pullRequestId } =
|
|
244
|
-
parsePullRequestLocation(urlValue);
|
|
220
|
+
const requestedPath = inputUrl.searchParams.get('path');
|
|
221
|
+
const { apiRoot, repository, pullRequestId } = parsePullRequestLocation(urlValue);
|
|
245
222
|
const encodedRepository = encodeURIComponent(repository);
|
|
246
223
|
const repositoryRoot = `${apiRoot}/_apis/git/repositories/${encodedRepository}`;
|
|
247
224
|
const root = `${repositoryRoot}/pullRequests/${pullRequestId}`;
|
|
248
225
|
const headers = {
|
|
249
|
-
Accept:
|
|
250
|
-
Authorization: `Basic ${Buffer.from(`:${pat}`).toString(
|
|
226
|
+
Accept: 'application/json',
|
|
227
|
+
Authorization: `Basic ${Buffer.from(`:${pat}`).toString('base64')}`
|
|
251
228
|
};
|
|
252
229
|
|
|
253
230
|
const [pullRequest, threads, workItems, iterations] = await Promise.all([
|
|
254
231
|
azureGet(`${root}?api-version=7.1`, headers),
|
|
255
232
|
azureGet(`${root}/threads?api-version=7.1`, headers, true),
|
|
256
233
|
azureGet(`${root}/workitems?api-version=7.1`, headers, true),
|
|
257
|
-
azureGet(`${root}/iterations?api-version=7.1`, headers, true)
|
|
234
|
+
azureGet(`${root}/iterations?api-version=7.1`, headers, true)
|
|
258
235
|
]);
|
|
259
|
-
const latestIterationId = Math.max(
|
|
260
|
-
0,
|
|
261
|
-
...(iterations?.value || []).map(({ id }) => Number(id) || 0),
|
|
262
|
-
);
|
|
236
|
+
const latestIterationId = Math.max(0, ...(iterations?.value || []).map(({ id }) => Number(id) || 0));
|
|
263
237
|
const changes = latestIterationId
|
|
264
238
|
? await azureGet(
|
|
265
239
|
`${root}/iterations/${latestIterationId}/changes?$top=${MAX_CHANGED_FILES}&api-version=7.1`,
|
|
266
240
|
headers,
|
|
267
|
-
true
|
|
241
|
+
true
|
|
268
242
|
)
|
|
269
243
|
: undefined;
|
|
270
244
|
|
|
@@ -274,30 +248,26 @@ export async function getAzurePullRequest(urlValue, pat) {
|
|
|
274
248
|
.filter(({ item }) => !isAttachmentPath(item?.path))
|
|
275
249
|
.filter(({ item }) => isInsideRequestedPath(item?.path, requestedPath))
|
|
276
250
|
.slice(0, MAX_CHANGED_FILES);
|
|
277
|
-
const changedFiles = await mapWithConcurrency(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
addedContentTruncated: additions?.truncated || undefined,
|
|
298
|
-
};
|
|
299
|
-
},
|
|
300
|
-
);
|
|
251
|
+
const changedFiles = await mapWithConcurrency(changeEntries, FILE_FETCH_CONCURRENCY, async ({ changeType, item }) => {
|
|
252
|
+
const filePath = item?.path;
|
|
253
|
+
const deleted = String(changeType).toLowerCase().includes('delete');
|
|
254
|
+
const isFile = !item?.gitObjectType || item.gitObjectType === 'blob';
|
|
255
|
+
if (!filePath || deleted || !isFile || isBinaryPath(filePath)) {
|
|
256
|
+
return { changeType, path: filePath };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const [previousContent, currentContent] = await Promise.all([
|
|
260
|
+
azureItemContent(repositoryRoot, filePath, targetCommitId, headers),
|
|
261
|
+
azureItemContent(repositoryRoot, filePath, sourceCommitId, headers)
|
|
262
|
+
]);
|
|
263
|
+
const additions = extractAddedContent(previousContent, currentContent);
|
|
264
|
+
return {
|
|
265
|
+
changeType,
|
|
266
|
+
path: filePath,
|
|
267
|
+
addedContent: additions?.content || undefined,
|
|
268
|
+
addedContentTruncated: additions?.truncated || undefined
|
|
269
|
+
};
|
|
270
|
+
});
|
|
301
271
|
|
|
302
272
|
return {
|
|
303
273
|
title: truncateText(pullRequest.title, 2_000),
|
|
@@ -307,21 +277,21 @@ export async function getAzurePullRequest(urlValue, pat) {
|
|
|
307
277
|
targetBranch: pullRequest.targetRefName,
|
|
308
278
|
reviewers: (pullRequest.reviewers || []).map(({ displayName, vote }) => ({
|
|
309
279
|
displayName: truncateText(displayName, 1_000),
|
|
310
|
-
vote
|
|
280
|
+
vote
|
|
311
281
|
})),
|
|
312
282
|
reviewComments: (threads?.value || [])
|
|
313
|
-
.flatMap(
|
|
283
|
+
.flatMap(thread =>
|
|
314
284
|
(thread.comments || [])
|
|
315
|
-
.filter(
|
|
285
|
+
.filter(comment => !comment.isDeleted && comment.content)
|
|
316
286
|
.map(({ content, author }) => ({
|
|
317
287
|
content: truncateText(content, MAX_COMMENT_LENGTH),
|
|
318
|
-
author: truncateText(author?.displayName, 1_000)
|
|
319
|
-
}))
|
|
288
|
+
author: truncateText(author?.displayName, 1_000)
|
|
289
|
+
}))
|
|
320
290
|
)
|
|
321
291
|
.slice(0, MAX_REVIEW_COMMENTS),
|
|
322
292
|
workItems: (workItems?.value || []).map(({ id, url }) => ({ id, url })),
|
|
323
293
|
requestedPath: requestedPath || undefined,
|
|
324
294
|
latestIterationId,
|
|
325
|
-
changedFiles
|
|
295
|
+
changedFiles
|
|
326
296
|
};
|
|
327
297
|
}
|
|
@@ -1,65 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { McpServer } from
|
|
4
|
-
import { StdioServerTransport } from
|
|
5
|
-
import { z } from
|
|
6
|
-
import { getAzurePullRequest, serializePullRequest } from
|
|
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
7
|
|
|
8
8
|
const server = new McpServer(
|
|
9
|
-
{ name:
|
|
9
|
+
{ name: 'azurepr-mcp-bridge', version: '1.0.0' },
|
|
10
10
|
{
|
|
11
11
|
instructions:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
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
16
|
);
|
|
17
17
|
|
|
18
18
|
server.registerTool(
|
|
19
|
-
|
|
19
|
+
'get_azure_pull_request',
|
|
20
20
|
{
|
|
21
|
-
title:
|
|
21
|
+
title: '读取 Azure DevOps PR',
|
|
22
22
|
description:
|
|
23
|
-
|
|
23
|
+
'读取 PR 基本信息、评审意见、关联工作项和变更文件。URL 可使用 path 查询参数限定仓库目录,例如 ?path=/src。',
|
|
24
24
|
inputSchema: {
|
|
25
|
-
url: z
|
|
26
|
-
.url()
|
|
27
|
-
.describe("dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址"),
|
|
25
|
+
url: z.url().describe('dev.azure.com 或 *.visualstudio.com 的 Azure DevOps PR 地址')
|
|
28
26
|
},
|
|
29
27
|
annotations: {
|
|
30
28
|
readOnlyHint: true,
|
|
31
29
|
destructiveHint: false,
|
|
32
30
|
idempotentHint: true,
|
|
33
|
-
openWorldHint: true
|
|
34
|
-
}
|
|
31
|
+
openWorldHint: true
|
|
32
|
+
}
|
|
35
33
|
},
|
|
36
34
|
async ({ url }) => {
|
|
37
35
|
try {
|
|
38
36
|
const pat = process.env.AZURE_DEVOPS_PAT?.trim();
|
|
39
37
|
if (!pat) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
"AZURE_DEVOPS_PAT 未配置,Stdio MCP 无法读取 Azure DevOps PR",
|
|
42
|
-
);
|
|
38
|
+
throw new Error('AZURE_DEVOPS_PAT 未配置,Stdio MCP 无法读取 Azure DevOps PR');
|
|
43
39
|
}
|
|
44
40
|
const result = await getAzurePullRequest(url, pat);
|
|
45
41
|
return {
|
|
46
|
-
content: [{ type:
|
|
42
|
+
content: [{ type: 'text', text: serializePullRequest(result) }]
|
|
47
43
|
};
|
|
48
44
|
} catch (error) {
|
|
49
45
|
const message = error instanceof Error ? error.message : String(error);
|
|
50
46
|
return {
|
|
51
47
|
isError: true,
|
|
52
|
-
content: [{ type:
|
|
48
|
+
content: [{ type: 'text', text: message }]
|
|
53
49
|
};
|
|
54
50
|
}
|
|
55
|
-
}
|
|
51
|
+
}
|
|
56
52
|
);
|
|
57
53
|
|
|
58
54
|
async function main() {
|
|
59
55
|
await server.connect(new StdioServerTransport());
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
main().catch(
|
|
63
|
-
console.error(
|
|
58
|
+
main().catch(error => {
|
|
59
|
+
console.error('Failed to start azurepr-mcp-bridge:', error);
|
|
64
60
|
process.exitCode = 1;
|
|
65
61
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import assert from
|
|
2
|
-
import path from
|
|
3
|
-
import test from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { Client } from
|
|
6
|
-
import { StdioClientTransport } from
|
|
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
7
|
import {
|
|
8
8
|
extractAddedContent,
|
|
9
9
|
isAttachmentPath,
|
|
@@ -11,66 +11,56 @@ import {
|
|
|
11
11
|
isInsideRequestedPath,
|
|
12
12
|
MAX_CONTENT_LENGTH,
|
|
13
13
|
parsePullRequestLocation,
|
|
14
|
-
serializePullRequest
|
|
15
|
-
} from
|
|
14
|
+
serializePullRequest
|
|
15
|
+
} from './azure-devops.js';
|
|
16
16
|
|
|
17
17
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
|
|
19
|
-
test(
|
|
19
|
+
test('parses supported Azure DevOps PR URLs', () => {
|
|
20
20
|
assert.deepEqual(
|
|
21
|
-
parsePullRequestLocation(
|
|
22
|
-
"https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
|
|
23
|
-
),
|
|
21
|
+
parsePullRequestLocation('https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'),
|
|
24
22
|
{
|
|
25
|
-
apiRoot:
|
|
26
|
-
repository:
|
|
27
|
-
pullRequestId:
|
|
28
|
-
}
|
|
23
|
+
apiRoot: 'https://autobest.visualstudio.com/AutoBestChina',
|
|
24
|
+
repository: 'web',
|
|
25
|
+
pullRequestId: '42'
|
|
26
|
+
}
|
|
29
27
|
);
|
|
30
28
|
assert.deepEqual(
|
|
31
|
-
parsePullRequestLocation(
|
|
32
|
-
"https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7",
|
|
33
|
-
),
|
|
29
|
+
parsePullRequestLocation('https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7'),
|
|
34
30
|
{
|
|
35
|
-
apiRoot:
|
|
36
|
-
repository:
|
|
37
|
-
pullRequestId:
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
assert.throws(
|
|
41
|
-
() =>
|
|
42
|
-
parsePullRequestLocation(
|
|
43
|
-
"https://example.com/org/project/_git/repo/pullrequest/1",
|
|
44
|
-
),
|
|
45
|
-
/仅支持/,
|
|
31
|
+
apiRoot: 'https://dev.azure.com/example/My%20Project',
|
|
32
|
+
repository: 'front end',
|
|
33
|
+
pullRequestId: '7'
|
|
34
|
+
}
|
|
46
35
|
);
|
|
36
|
+
assert.throws(() => parsePullRequestLocation('https://example.com/org/project/_git/repo/pullrequest/1'), /仅支持/);
|
|
47
37
|
});
|
|
48
38
|
|
|
49
|
-
test(
|
|
50
|
-
assert.equal(isAttachmentPath(
|
|
51
|
-
assert.equal(isBinaryPath(
|
|
52
|
-
assert.equal(isInsideRequestedPath(
|
|
53
|
-
assert.equal(isInsideRequestedPath(
|
|
54
|
-
assert.deepEqual(extractAddedContent(
|
|
55
|
-
content:
|
|
56
|
-
truncated: false
|
|
39
|
+
test('filters paths and extracts added lines', () => {
|
|
40
|
+
assert.equal(isAttachmentPath('/docs/.attachments/image.png'), true);
|
|
41
|
+
assert.equal(isBinaryPath('/assets/logo.PNG'), true);
|
|
42
|
+
assert.equal(isInsideRequestedPath('/src/app/index.js', '/src'), true);
|
|
43
|
+
assert.equal(isInsideRequestedPath('/scripts/build.js', '/src'), false);
|
|
44
|
+
assert.deepEqual(extractAddedContent('a\nb\n', 'a\nc\nb\n'), {
|
|
45
|
+
content: 'c\n',
|
|
46
|
+
truncated: false
|
|
57
47
|
});
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
test(
|
|
50
|
+
test('serializes oversized results as valid bounded JSON', () => {
|
|
61
51
|
const text = serializePullRequest({
|
|
62
|
-
title:
|
|
63
|
-
description:
|
|
64
|
-
status:
|
|
52
|
+
title: 'Large PR',
|
|
53
|
+
description: 'description',
|
|
54
|
+
status: 'active',
|
|
65
55
|
reviewers: [],
|
|
66
56
|
reviewComments: [],
|
|
67
57
|
workItems: [],
|
|
68
58
|
latestIterationId: 1,
|
|
69
59
|
changedFiles: Array.from({ length: 20 }, (_, index) => ({
|
|
70
|
-
changeType:
|
|
60
|
+
changeType: 'edit',
|
|
71
61
|
path: `/src/file-${index}.js`,
|
|
72
|
-
addedContent:
|
|
73
|
-
}))
|
|
62
|
+
addedContent: 'x'.repeat(20_000)
|
|
63
|
+
}))
|
|
74
64
|
});
|
|
75
65
|
|
|
76
66
|
assert.ok(text.length <= MAX_CONTENT_LENGTH);
|
|
@@ -78,35 +68,36 @@ test("serializes oversized results as valid bounded JSON", () => {
|
|
|
78
68
|
assert.ok(JSON.parse(text).addedContentOmittedDueToResponseLimit > 0);
|
|
79
69
|
});
|
|
80
70
|
|
|
81
|
-
test(
|
|
71
|
+
test('initializes and exposes the Azure PR tool over stdio', async () => {
|
|
82
72
|
const client = new Client({
|
|
83
|
-
name:
|
|
84
|
-
version:
|
|
73
|
+
name: 'azurepr-mcp-bridge-test',
|
|
74
|
+
version: '1.0.0'
|
|
85
75
|
});
|
|
86
76
|
const transport = new StdioClientTransport({
|
|
87
77
|
command: process.execPath,
|
|
88
|
-
args: [path.join(directory,
|
|
89
|
-
cwd: path.resolve(directory,
|
|
90
|
-
stderr:
|
|
78
|
+
args: [path.join(directory, 'index.js')],
|
|
79
|
+
cwd: path.resolve(directory, '../..'),
|
|
80
|
+
stderr: 'pipe'
|
|
91
81
|
});
|
|
92
82
|
|
|
93
83
|
try {
|
|
94
84
|
await client.connect(transport);
|
|
95
|
-
assert.equal(client.getServerVersion()?.name,
|
|
96
|
-
assert.match(client.getInstructions() ??
|
|
85
|
+
assert.equal(client.getServerVersion()?.name, 'azurepr-mcp-bridge');
|
|
86
|
+
assert.match(client.getInstructions() ?? '', /AZURE_DEVOPS_PAT/);
|
|
97
87
|
|
|
98
88
|
const { tools } = await client.listTools();
|
|
99
|
-
assert.deepEqual(
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
tools.map(({ name }) => name),
|
|
91
|
+
'get_azure_pull_request'
|
|
92
|
+
);
|
|
102
93
|
assert.equal(tools[0].annotations?.readOnlyHint, true);
|
|
103
94
|
assert.equal(tools[0].annotations?.openWorldHint, true);
|
|
104
95
|
|
|
105
96
|
const missingPatResult = await client.callTool({
|
|
106
|
-
name:
|
|
97
|
+
name: 'get_azure_pull_request',
|
|
107
98
|
arguments: {
|
|
108
|
-
url:
|
|
109
|
-
}
|
|
99
|
+
url: 'https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'
|
|
100
|
+
}
|
|
110
101
|
});
|
|
111
102
|
assert.equal(missingPatResult.isError, true);
|
|
112
103
|
assert.match(missingPatResult.content[0].text, /AZURE_DEVOPS_PAT 未配置/);
|