@autobest-ui/agent 1.0.0 → 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 +69 -10
- 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/LICENSE +21 -0
- package/mcp/figma-mcp-bridge/README.md +25 -0
- package/mcp/figma-mcp-bridge/config.toml.example +10 -0
- package/mcp/figma-mcp-bridge/index.test.js +47 -0
- package/mcp/figma-mcp-bridge/package.json +16 -0
- package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
- package/mcp/figma-mcp-bridge/src/index.js +68 -0
- package/mcp/figma-mcp-bridge/src/server.js +159 -0
- package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
- package/mcp/figma-mcp-bridge/src/tools/index.js +2148 -0
- package/mcp/figma-mcp-bridge/src/tools/mutations.js +5829 -0
- package/mcp/figma-mcp-bridge/src/tools/nodes.js +99 -0
- package/mcp/figma-mcp-bridge/src/tools/pages.js +70 -0
- package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
- package/mcp/rag-mcp-bridge/README.md +24 -12
- 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 +9 -4
- package/plugins/figma-plugin/LICENSE +21 -0
- package/plugins/figma-plugin/README.md +25 -0
- package/plugins/figma-plugin/code.js +6608 -0
- package/plugins/figma-plugin/manifest.json +32 -0
- package/plugins/figma-plugin/scripts/setup.mjs +72 -0
- package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
- package/plugins/figma-plugin/ui.html +235 -0
package/README.md
CHANGED
|
@@ -20,30 +20,63 @@ npx --yes --package=@autobest-ui/agent@latest autobest-agent-sync react
|
|
|
20
20
|
|
|
21
21
|
## MCP
|
|
22
22
|
|
|
23
|
-
安装 Node.js 20
|
|
23
|
+
安装 Node.js 20 或更高版本后,无需在业务项目中安装依赖。直接在终端验证 MCP 时,必须同时提供对应的环境变量。
|
|
24
|
+
|
|
25
|
+
先启动 PRD Knowledge HTTP API,再验证 RAG MCP:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
|
|
29
|
+
npx --yes \
|
|
30
|
+
--package=@autobest-ui/agent@latest \
|
|
31
|
+
autobest-rag-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
验证 Azure PR MCP:
|
|
24
35
|
|
|
25
36
|
```bash
|
|
26
|
-
|
|
27
|
-
npx --yes
|
|
37
|
+
AZURE_DEVOPS_PAT="your-personal-access-token" \
|
|
38
|
+
npx --yes \
|
|
39
|
+
--package=@autobest-ui/agent@latest \
|
|
40
|
+
autobest-azurepr-mcp
|
|
28
41
|
```
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
启动成功后,MCP 会通过 stdio 持续等待客户端请求,终端没有输出属于正常状态,可按 `Ctrl+C` 停止。供 Codex 长期使用时,不要只执行上述临时命令,应将完整配置加入 `~/.codex/config.toml` 后重启 Codex。
|
|
44
|
+
|
|
45
|
+
同一个 npm 包暴露三个独立命令:
|
|
31
46
|
|
|
32
47
|
| 命令 | 用途 | 必需配置 |
|
|
33
48
|
| ---------------------- | ------------------------------ | ------------------ |
|
|
34
49
|
| `autobest-rag-mcp` | 连接 PRD Knowledge RAG API | `RAG_API_BASE_URL` |
|
|
35
50
|
| `autobest-azurepr-mcp` | 读取 Azure DevOps Pull Request | `AZURE_DEVOPS_PAT` |
|
|
51
|
+
| `figma-mcp-bridge` | 连接本机 Figma Plugin | 无 |
|
|
36
52
|
|
|
37
53
|
Codex 配置示例分别位于:
|
|
38
54
|
|
|
39
55
|
- [RAG MCP 配置](mcp/rag-mcp-bridge/config.toml.example)
|
|
40
56
|
- [Azure PR MCP 配置](mcp/azurepr-mcp-bridge/config.toml.example)
|
|
57
|
+
- [Figma MCP 配置](mcp/figma-mcp-bridge/config.toml.example)
|
|
41
58
|
|
|
42
59
|
生产环境可将 `@latest` 替换为明确版本,以固定 MCP 行为。
|
|
43
60
|
|
|
44
61
|
如果 `@autobest-ui` 发布在私有 npm registry,使用前需要在 npm 的 `.npmrc` 中配置该 scope 对应的 registry 和认证信息。
|
|
45
62
|
|
|
46
|
-
## Plugin
|
|
63
|
+
## Figma Plugin
|
|
64
|
+
|
|
65
|
+
Figma Plugin 与 `figma-mcp-bridge` 分目录维护,并通过同一个 npm 包提供两个独立命令。安装或更新到稳定目录:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx --yes --package=@autobest-ui/agent@latest figma-plugin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
命令会输出 `manifest.json` 的稳定路径。首次安装后,在 Figma 中通过 **Plugins -> Development -> Import plugin from manifest** 导入该文件。详细说明见 [Figma Plugin 使用说明](plugins/figma-plugin/README.md)。
|
|
72
|
+
|
|
73
|
+
卸载本地文件:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx --yes --package=@autobest-ui/agent@latest figma-plugin uninstall
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Codex Plugin
|
|
47
80
|
|
|
48
81
|
Autobest Delivery 将交付闭环 Skills 和隔离的 Playwright MCP 运行器作为一个 Codex Plugin 安装。需要 Node.js 20 或更高版本,以及支持 `codex plugin` 命令的 Codex CLI。
|
|
49
82
|
|
|
@@ -126,7 +159,7 @@ Common Skills 会写入 `~/.agents/skills`,React Skills 会写入目标项目
|
|
|
126
159
|
RAG MCP:
|
|
127
160
|
|
|
128
161
|
```bash
|
|
129
|
-
RAG_API_BASE_URL=http://
|
|
162
|
+
RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
|
|
130
163
|
npx --yes \
|
|
131
164
|
--package="$AGENT_TARBALL_PATH" \
|
|
132
165
|
autobest-rag-mcp
|
|
@@ -141,6 +174,22 @@ npx --yes \
|
|
|
141
174
|
autobest-azurepr-mcp
|
|
142
175
|
```
|
|
143
176
|
|
|
177
|
+
Figma MCP:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx --yes \
|
|
181
|
+
--package="$AGENT_TARBALL_PATH" \
|
|
182
|
+
figma-mcp-bridge
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Figma Plugin:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx --yes \
|
|
189
|
+
--package="$AGENT_TARBALL_PATH" \
|
|
190
|
+
figma-plugin
|
|
191
|
+
```
|
|
192
|
+
|
|
144
193
|
MCP 使用 stdio 协议,启动后持续等待客户端请求属于正常行为,可按 `Ctrl+C` 停止。
|
|
145
194
|
|
|
146
195
|
### 5. 在 Codex 中联调 MCP
|
|
@@ -157,7 +206,7 @@ args = [
|
|
|
157
206
|
]
|
|
158
207
|
|
|
159
208
|
[mcp_servers.rag-mcp-bridge.env]
|
|
160
|
-
RAG_API_BASE_URL = "http://
|
|
209
|
+
RAG_API_BASE_URL = "http://192.168.1.12:3000/api/knowledge"
|
|
161
210
|
|
|
162
211
|
[mcp_servers.azurepr-mcp-bridge]
|
|
163
212
|
command = "npx"
|
|
@@ -167,11 +216,21 @@ args = [
|
|
|
167
216
|
"autobest-azurepr-mcp"
|
|
168
217
|
]
|
|
169
218
|
|
|
170
|
-
[
|
|
171
|
-
|
|
219
|
+
env_vars = ["AZURE_DEVOPS_PAT"]
|
|
220
|
+
|
|
221
|
+
[mcp_servers.figma-mcp-bridge]
|
|
222
|
+
command = "npx"
|
|
223
|
+
args = [
|
|
224
|
+
"--yes",
|
|
225
|
+
"--package=/absolute/path/to/autobest-ui-agent-x.y.z.tgz",
|
|
226
|
+
"figma-mcp-bridge"
|
|
227
|
+
]
|
|
228
|
+
startup_timeout_sec = 30
|
|
229
|
+
tool_timeout_sec = 120
|
|
230
|
+
enabled = true
|
|
172
231
|
```
|
|
173
232
|
|
|
174
|
-
|
|
233
|
+
配置 Azure PR MCP 前,还需要在启动 Codex 的环境中设置 `AZURE_DEVOPS_PAT`。修改 `config.toml` 后重启 Codex,再从新会话调用对应 MCP 工具。重新打包后如果 tarball 文件名或路径发生变化,需要同步更新 `config.toml`。
|
|
175
234
|
|
|
176
235
|
## 本地验证
|
|
177
236
|
|
|
@@ -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
|
});
|