@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.
Files changed (30) hide show
  1. package/README.md +69 -10
  2. package/mcp/azurepr-mcp-bridge/azure-devops.js +82 -112
  3. package/mcp/azurepr-mcp-bridge/index.js +21 -25
  4. package/mcp/azurepr-mcp-bridge/index.test.js +50 -59
  5. package/mcp/figma-mcp-bridge/LICENSE +21 -0
  6. package/mcp/figma-mcp-bridge/README.md +25 -0
  7. package/mcp/figma-mcp-bridge/config.toml.example +10 -0
  8. package/mcp/figma-mcp-bridge/index.test.js +47 -0
  9. package/mcp/figma-mcp-bridge/package.json +16 -0
  10. package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
  11. package/mcp/figma-mcp-bridge/src/index.js +68 -0
  12. package/mcp/figma-mcp-bridge/src/server.js +159 -0
  13. package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
  14. package/mcp/figma-mcp-bridge/src/tools/index.js +2148 -0
  15. package/mcp/figma-mcp-bridge/src/tools/mutations.js +5829 -0
  16. package/mcp/figma-mcp-bridge/src/tools/nodes.js +99 -0
  17. package/mcp/figma-mcp-bridge/src/tools/pages.js +70 -0
  18. package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
  19. package/mcp/rag-mcp-bridge/README.md +24 -12
  20. package/mcp/rag-mcp-bridge/config.toml.example +1 -1
  21. package/mcp/rag-mcp-bridge/index.js +80 -125
  22. package/mcp/rag-mcp-bridge/index.test.js +21 -25
  23. package/package.json +9 -4
  24. package/plugins/figma-plugin/LICENSE +21 -0
  25. package/plugins/figma-plugin/README.md +25 -0
  26. package/plugins/figma-plugin/code.js +6608 -0
  27. package/plugins/figma-plugin/manifest.json +32 -0
  28. package/plugins/figma-plugin/scripts/setup.mjs +72 -0
  29. package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
  30. 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 或更高版本后,无需在业务项目中安装依赖,可以直接通过 `npx` 启动 MCP:
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
- npx --yes --package=@autobest-ui/agent@latest autobest-rag-mcp
27
- npx --yes --package=@autobest-ui/agent@latest autobest-azurepr-mcp
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
- 同一个 npm 包暴露两个独立命令:
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://127.0.0.1:3000 \
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://127.0.0.1:3000"
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
- [mcp_servers.azurepr-mcp-bridge.env]
171
- AZURE_DEVOPS_PAT = "your-token"
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
- 修改 `config.toml` 后重启 Codex,再从新会话调用对应 MCP 工具。重新打包后如果 tarball 文件名或路径发生变化,需要同步更新 `config.toml`。
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 "diff";
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
- ".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",
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 !== "string" || !filePath) return false;
44
- if (!requestedPath || requestedPath === "/") return true;
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("Azure DevOps PR 地址包含无效编码");
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 !== "https:") {
63
- throw new Error("Azure DevOps PR 地址必须使用 HTTPS");
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(".visualstudio.com");
68
- const isDevAzure = hostname === "dev.azure.com";
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("无法识别 Azure DevOps PR 地址");
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: "true",
118
- "versionDescriptor.version": commitId,
119
- "versionDescriptor.versionType": "commit",
120
- "api-version": "7.1",
110
+ includeContent: 'true',
111
+ 'versionDescriptor.version': commitId,
112
+ 'versionDescriptor.versionType': 'commit',
113
+ 'api-version': '7.1'
121
114
  });
122
- const item = await azureGet(
123
- `${repositoryRoot}/items?${query.toString()}`,
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 !== "string") return undefined;
124
+ if (typeof currentContent !== 'string') return undefined;
136
125
 
137
126
  const additions =
138
- typeof previousContent === "string"
127
+ typeof previousContent === 'string'
139
128
  ? diffLines(previousContent, currentContent)
140
- .filter((part) => part.added)
141
- .map((part) => part.value)
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 !== "string") return 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: "PR 响应超过 MCP 内容上限",
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("path");
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: "application/json",
250
- Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}`,
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
- 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
- );
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((thread) =>
283
+ .flatMap(thread =>
314
284
  (thread.comments || [])
315
- .filter((comment) => !comment.isDeleted && comment.content)
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 "@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";
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: "azurepr-mcp-bridge", version: "1.0.0" },
9
+ { name: 'azurepr-mcp-bridge', version: '1.0.0' },
10
10
  {
11
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
- },
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
- "get_azure_pull_request",
19
+ 'get_azure_pull_request',
20
20
  {
21
- title: "读取 Azure DevOps PR",
21
+ title: '读取 Azure DevOps PR',
22
22
  description:
23
- "读取 PR 基本信息、评审意见、关联工作项和变更文件。URL 可使用 path 查询参数限定仓库目录,例如 ?path=/src。",
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: "text", text: serializePullRequest(result) }],
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: "text", text: message }],
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((error) => {
63
- console.error("Failed to start azurepr-mcp-bridge:", error);
58
+ main().catch(error => {
59
+ console.error('Failed to start azurepr-mcp-bridge:', error);
64
60
  process.exitCode = 1;
65
61
  });