1. 为什么我要自己造一个自动提 PR 的工具先说结论我受够了每次改完代码还要手动切分支、写 commit message、推远端、开浏览器、填 PR 标题和描述这一整套流程。尤其是当你在一个迭代周期里要反复提交十几个小改动时这种重复劳动简直是在消耗生命。我日常的主力代码托管平台是 Azure DevOps团队用的是标准的 Pull Request 工作流。Git 本身的操作并不复杂但问题在于“流程编排”——你得记住当前分支名、目标分支名、关联的工作项 ID、PR 描述模板还要确保没有漏掉 reviewer。这些信息散落在各个地方每次手动操作都像是在做一道填空题。于是我开始琢磨能不能做一个工具让我在终端里敲一行命令剩下的全自动完成更进一步能不能让 AI 助手直接帮我完成这件事这就是我动手实现这个 MCP 工具的起点。MCPModel Context Protocol是 Anthropic 推出的一个开放协议简单说就是让 AI 助手能够调用外部工具的标准化接口。你可以把它理解成“AI 世界的 USB 接口”——只要你的工具实现了 MCP 协议任何支持 MCP 的 AI 客户端都能直接调用它。而Skill在这里扮演的是“能力封装”的角色把 Git 操作和 Azure DevOps API 调用串成一条完整的流水线。这个工具适合谁如果你满足以下任意一条这篇文章就是写给你的每天要在 Azure DevOps 上提交多个 PR手动操作已经让你厌烦想了解 MCP 协议的实际落地方式而不只是停留在概念层面对 AI 助手 工具链的自动化编排感兴趣想自己动手做一个团队有标准化的 PR 流程但缺乏自动化手段我最终实现的效果是在支持 MCP 的 AI 客户端里说一句“帮我把当前改动提交 PR 到 develop 分支”工具会自动完成分支检查、文件暂存、commit、push、创建 PR 的全流程并且返回 PR 链接。整个过程不需要离开对话界面。下面我把整个实现过程拆开来讲包括架构设计、核心代码、踩过的坑以及一些只有实际跑过才会知道的细节。2. 整体架构设计与技术选型思路2.1 为什么选 MCP 而不是写个 Shell 脚本最开始我确实想过写个 Bash 脚本就完事了。git add、git commit、git push、然后调 Azure DevOps 的 REST API 创建 PR一个脚本几十行就能搞定。但实际用下来发现几个问题第一参数传递不灵活。Shell 脚本处理 JSON 响应很痛苦尤其是 Azure DevOps 返回的 PR 对象嵌套层级很深用 jq 解析虽然能行但可读性和可维护性都很差。第二无法与 AI 助手集成。Shell 脚本只能我自己在终端跑没法让 AI 助手理解“当前仓库状态”并做出决策。比如 AI 需要先判断当前分支是否有未提交的改动、是否已经存在同名 PR这些逻辑用 Shell 写会非常臃肿。第三错误处理粗糙。Git 操作和 API 调用都可能失败Shell 脚本的错误处理基本靠set -e和手动判断退出码很难做到精细化的错误分类和重试。MCP 的优势在于它天然就是为“工具调用”设计的。每个工具都有明确的输入 schema 和输出格式AI 助手能理解每个工具做什么、需要什么参数、返回什么结果。这比 Shell 脚本的“黑盒”模式强太多。2.2 Skill 的定位编排层而非执行层这里要澄清一个概念Skill 在我的设计里不是“执行具体操作”的模块而是“编排流程”的模块。具体来说执行层Git 命令封装、Azure DevOps API 调用这些是原子操作编排层Skill决定什么时候调用哪个原子操作、如何处理中间结果、如何应对异常打个比方执行层像是厨房里的各种厨具——刀、锅、铲子Skill 像是菜谱——告诉你先切菜还是先热油、什么时候放盐、火候怎么控制。没有菜谱你有一堆厨具也做不出一道菜。我选择把 Skill 独立出来的另一个原因是可测试性。编排逻辑可以单独写单元测试mock 掉底层的 Git 和 API 调用验证流程是否正确。如果全部揉在一起测试会变得非常困难。2.3 技术栈选择与理由最终的技术栈如下组件选型理由运行时Node.js 18MCP SDK 对 Node.js 支持最好生态成熟语言TypeScript类型安全MCP 的 schema 定义需要强类型支撑Git 操作simple-git比 child_process 调 git 命令更安全API 友好HTTP 请求axios拦截器机制方便统一处理 Azure DevOps 的认证和错误MCP SDKmodelcontextprotocol/sdk官方 SDK文档完善构建工具tsup零配置打包输出 ESM CJS 双格式为什么不用 PythonMCP 的 Python SDK 确实存在但 Node.js 版本更新更频繁社区示例也更多。而且 simple-git 这个库的质量非常高省去了很多自己封装 Git 命令的工作。为什么不用 GoGo 的 MCP 生态还不够成熟而且我在这个项目里更看重开发速度而非运行时性能。Git 操作和 API 调用都是 IO 密集型Node.js 的异步模型完全够用。2.4 整体数据流设计整个工具的数据流是这样的AI 客户端发起工具调用传入参数目标分支、PR 标题等MCP Server 接收请求交给 Skill 编排层Skill 首先调用 Git 模块检查仓库状态当前分支、是否有未提交改动如果有未提交改动调用 Git 模块执行 add commit调用 Git 模块 push 到远端调用 Azure DevOps 模块创建 PR返回 PR 链接给 AI 客户端这个流程看起来简单但每一步都有需要处理的边界情况。比如当前分支就是目标分支怎么办远端已经存在同名分支怎么办PR 已经存在怎么办这些都会在后面详细展开。3. 核心模块拆解与关键实现细节3.1 Git 操作模块不只是调命令那么简单很多人觉得封装 Git 操作就是exec(git add .)这么简单但实际做下来你会发现有一堆细节要处理。首先是仓库状态检测。在提交之前你必须知道当前仓库处于什么状态import simpleGit, { SimpleGit } from simple-git; interface RepoStatus { currentBranch: string; hasUncommittedChanges: boolean; stagedFiles: string[]; unstagedFiles: string[]; untrackedFiles: string[]; aheadCount: number; behindCount: number; } async function getRepoStatus(repoPath: string): PromiseRepoStatus { const git: SimpleGit simpleGit(repoPath); const status await git.status(); return { currentBranch: status.current || , hasUncommittedChanges: !status.isClean(), stagedFiles: status.staged, unstagedFiles: status.modified, untrackedFiles: status.not_added, aheadCount: status.ahead, behindCount: status.behind, }; }这段代码看起来直白但有几个坑我踩过坑一detached HEAD 状态。如果用户处于 detached HEAD 状态status.current会返回空字符串或者 commit hash。这时候直接提交会导致 commit 丢失。我的处理方式是检测到 detached HEAD 时直接报错提示用户先切换到正常分支。坑二未跟踪文件太多。有些项目里node_modules或者构建产物没有被.gitignore正确忽略导致untrackedFiles列表巨大。如果直接git add .会把这些文件全部加进去。我的做法是默认只 add 已跟踪文件的改动未跟踪文件需要显式指定。坑三换行符问题。Windows 和 Unix 的换行符不同如果团队里有人用 Windows 有人用 MacGit 可能会把所有文件标记为 modified。这个需要在.gitattributes里配置工具层面能做的不多但至少要在文档里提醒。接下来是分支操作。创建 PR 之前需要确保当前分支已经推送到远端async function ensureBranchPushed( git: SimpleGit, branchName: string, remote: string origin ): Promisevoid { const remotes await git.getRemotes(true); const targetRemote remotes.find(r r.name remote); if (!targetRemote) { throw new Error(Remote ${remote} not found); } const localBranches await git.branchLocal(); if (!localBranches.all.includes(branchName)) { throw new Error(Local branch ${branchName} does not exist); } const remoteBranches await git.branch([-r]); const remoteBranchName ${remote}/${branchName}; if (remoteBranches.all.includes(remoteBranchName)) { // 远端已存在检查是否需要 force push const localHash await git.revparse([branchName]); const remoteHash await git.revparse([remoteBranchName]); if (localHash ! remoteHash) { await git.push(remote, branchName); } } else { // 远端不存在设置 upstream 并推送 await git.push([-u, remote, branchName]); } }这里的关键点是不要无脑 force push。force push 会覆盖远端历史如果其他人已经基于这个分支做了改动会造成灾难性的后果。我的策略是如果远端分支存在且与本地不一致先尝试普通 push如果失败说明有冲突报错让用户手动处理。3.2 Azure DevOps API 封装认证与错误处理Azure DevOps 的 REST API 认证方式有几种PATPersonal Access Token、OAuth、Managed Identity。对于个人工具来说PAT 是最简单的方式。PAT 的权限配置很关键。创建 PR 需要以下权限Code (Read Write)读取仓库信息、创建 PRPull Request Threads (Read Write)如果需要添加评论或 reviewerWork Items (Read)如果需要关联工作项我见过有人给了 Full Access 的 PAT这是非常危险的。最小权限原则在这里同样适用。import axios, { AxiosInstance } from axios; interface AzureDevOpsConfig { organization: string; project: string; repository: string; pat: string; } function createApiClient(config: AzureDevOpsConfig): AxiosInstance { const baseURL https://dev.azure.com/${config.organization}/${config.project}/_apis; const client axios.create({ baseURL, headers: { Content-Type: application/json, Authorization: Basic ${Buffer.from(:${config.pat}).toString(base64)}, }, timeout: 30000, }); // 统一错误处理 client.interceptors.response.use( response response, error { if (error.response) { const { status, data } error.response; const message data?.message || error.message; switch (status) { case 401: throw new Error(Azure DevOps 认证失败请检查 PAT 是否有效: ${message}); case 403: throw new Error(权限不足请检查 PAT 的权限配置: ${message}); case 404: throw new Error(资源不存在请检查组织/项目/仓库名称: ${message}); case 409: throw new Error(冲突可能已存在相同的 PR: ${message}); default: throw new Error(Azure DevOps API 错误 (${status}): ${message}); } } throw error; } ); return client; }认证的坑Azure DevOps 的 PAT 认证用的是 HTTP Basic Auth但用户名部分可以是空的密码部分填 PAT。Base64 编码的格式是:PAT注意冒号前面是空的。我第一次写的时候把用户名填成了邮箱结果一直 401。API 版本号Azure DevOps 的 REST API 需要指定api-version参数。不同版本的 API 行为可能有差异。我固定用7.1版本这是目前最稳定的。3.3 创建 PR 的核心逻辑创建 PR 的 API 调用本身不复杂但参数构造需要仔细interface CreatePRParams { sourceBranch: string; targetBranch: string; title: string; description?: string; isDraft?: boolean; reviewers?: string[]; workItemIds?: number[]; } async function createPullRequest( client: AxiosInstance, config: AzureDevOpsConfig, params: CreatePRParams ): Promise{ prId: number; url: string } { // 分支名必须以 refs/heads/ 开头 const sourceRef params.sourceBranch.startsWith(refs/heads/) ? params.sourceBranch : refs/heads/${params.sourceBranch}; const targetRef params.targetBranch.startsWith(refs/heads/) ? params.targetBranch : refs/heads/${params.targetBranch}; const body: Recordstring, unknown { sourceRefName: sourceRef, targetRefName: targetRef, title: params.title, description: params.description || , isDraft: params.isDraft || false, }; if (params.reviewers params.reviewers.length 0) { body.reviewers params.reviewers.map(id ({ id })); } if (params.workItemIds params.workItemIds.length 0) { body.workItemRefs params.workItemIds.map(id ({ id: String(id) })); } const response await client.post( /git/repositories/${config.repository}/pullrequests?api-version7.1, body ); return { prId: response.data.pullRequestId, url: https://dev.azure.com/${config.organization}/${config.project}/_git/${config.repository}/pullrequest/${response.data.pullRequestId}, }; }关键细节sourceRefName和targetRefName必须以refs/heads/开头。如果你只传分支名比如feature/xxxAPI 会返回 400 错误。这个在官方文档里没有明确说明我是通过抓包才发现的。重复 PR 检测Azure DevOps 允许同一个源分支和目标分支之间存在多个 PR只要状态不同。但实际使用中如果已经有一个活跃的 PR再创建一个会让人困惑。我的做法是在创建之前先查询是否已存在活跃 PRasync function findExistingPR( client: AxiosInstance, config: AzureDevOpsConfig, sourceBranch: string, targetBranch: string ): Promisenumber | null { const sourceRef refs/heads/${sourceBranch}; const targetRef refs/heads/${targetBranch}; const response await client.get( /git/repositories/${config.repository}/pullrequests?api-version7.1, { params: { searchCriteria.sourceRefName: sourceRef, searchCriteria.targetRefName: targetRef, searchCriteria.status: active, }, } ); const prs response.data.value; return prs.length 0 ? prs[0].pullRequestId : null; }如果找到已存在的 PR我会返回它的链接而不是创建新的。这样避免了重复 PR 的问题。3.4 Skill 编排层把碎片串成流水线Skill 层的核心是一个状态机。每个步骤都有明确的输入、输出和失败处理interface SkillContext { repoPath: string; targetBranch: string; prTitle: string; prDescription?: string; commitMessage?: string; reviewers?: string[]; workItemIds?: number[]; isDraft?: boolean; } interface SkillResult { success: boolean; prUrl?: string; prId?: number; message: string; steps: StepResult[]; } interface StepResult { name: string; status: success | skipped | failed; detail: string; } async function executeAutoPR( context: SkillContext, git: SimpleGit, apiClient: AxiosInstance, config: AzureDevOpsConfig ): PromiseSkillResult { const steps: StepResult[] []; // Step 1: 检查仓库状态 const status await getRepoStatus(context.repoPath); steps.push({ name: check-repo-status, status: success, detail: 当前分支: ${status.currentBranch}, 未提交改动: ${status.hasUncommittedChanges}, }); // Step 2: 检查是否在目标分支上 if (status.currentBranch context.targetBranch) { return { success: false, message: 当前分支就是目标分支 ${context.targetBranch}无法创建 PR, steps, }; } // Step 3: 提交未暂存的改动 if (status.hasUncommittedChanges) { const commitMsg context.commitMessage || auto: update from ${status.currentBranch}; await git.add([-A]); await git.commit(commitMsg); steps.push({ name: commit-changes, status: success, detail: 已提交改动: ${commitMsg}, }); } else { steps.push({ name: commit-changes, status: skipped, detail: 没有未提交的改动, }); } // Step 4: 推送分支 await ensureBranchPushed(git, status.currentBranch); steps.push({ name: push-branch, status: success, detail: 已推送分支: ${status.currentBranch}, }); // Step 5: 检查是否已存在 PR const existingPR await findExistingPR( apiClient, config, status.currentBranch, context.targetBranch ); if (existingPR) { const url buildPRUrl(config, existingPR); steps.push({ name: create-pr, status: skipped, detail: 已存在活跃 PR #${existingPR}, }); return { success: true, prId: existingPR, prUrl: url, message: 已存在活跃 PR直接返回链接, steps, }; } // Step 6: 创建 PR const pr await createPullRequest(apiClient, config, { sourceBranch: status.currentBranch, targetBranch: context.targetBranch, title: context.prTitle, description: context.prDescription, isDraft: context.isDraft, reviewers: context.reviewers, workItemIds: context.workItemIds, }); steps.push({ name: create-pr, status: success, detail: 已创建 PR #${pr.prId}, }); return { success: true, prId: pr.prId, prUrl: pr.url, message: PR 创建成功, steps, }; }这个编排逻辑有几个设计决策值得说明为什么先 commit 再 push因为如果先 push 再 commitpush 的内容不包含最新的改动PR 里看到的代码就不是最新的。这个顺序不能反。为什么用git add -A而不是git add .git add .只添加当前目录下的改动如果仓库有多个子目录可能会漏掉。git add -A会添加所有改动包括删除的文件。当然前提是.gitignore配置正确。为什么检查已存在的 PR因为 AI 助手可能会重复调用同一个工具。如果用户说“帮我提个 PR”AI 执行了过一会儿用户又说“再提一次”如果没有去重逻辑就会创建两个 PR。这个在实际使用中非常常见。4. MCP Server 的注册与工具定义4.1 MCP 工具 Schema 设计MCP 工具的定义需要遵循 JSON Schema 规范。每个工具需要声明名称、描述和输入参数import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: auto-pr, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: auto_create_pr, description: 自动提交当前改动并创建 Azure DevOps Pull Request。会自动检测未提交的改动、提交、推送分支、创建 PR。, inputSchema: { type: object, properties: { repoPath: { type: string, description: Git 仓库的本地路径默认为当前工作目录, }, targetBranch: { type: string, description: PR 的目标分支例如 main、develop, default: main, }, prTitle: { type: string, description: PR 标题, }, prDescription: { type: string, description: PR 描述内容, }, commitMessage: { type: string, description: 提交信息如果不提供则自动生成, }, reviewers: { type: array, items: { type: string }, description: Reviewer 的用户 ID 列表, }, workItemIds: { type: array, items: { type: number }, description: 关联的工作项 ID 列表, }, isDraft: { type: boolean, description: 是否创建为草稿 PR, default: false, }, }, required: [prTitle], }, }, { name: check_repo_status, description: 检查 Git 仓库的当前状态包括当前分支、未提交改动、与远端的差异等。, inputSchema: { type: object, properties: { repoPath: { type: string, description: Git 仓库的本地路径, }, }, }, }, ], }));Schema 设计的经验第一description要写得足够详细。AI 助手是根据 description 来决定是否调用这个工具的。如果描述太简略AI 可能不知道什么时候该用它。比如我写的是“自动提交当前改动并创建 Azure DevOps Pull Request”而不是简单的“创建 PR”。第二required字段要精确。prTitle是必须的因为 Azure DevOps 要求 PR 必须有标题。其他参数都有合理的默认值不需要强制用户提供。第三参数命名要一致。我用的是 camelCase和 TypeScript 的惯例保持一致。有些 MCP 工具用 snake_case虽然也能工作但看起来不统一。4.2 工具调用的处理逻辑server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; try { switch (name) { case auto_create_pr: { const context: SkillContext { repoPath: (args?.repoPath as string) || process.cwd(), targetBranch: (args?.targetBranch as string) || main, prTitle: args?.prTitle as string, prDescription: args?.prDescription as string | undefined, commitMessage: args?.commitMessage as string | undefined, reviewers: args?.reviewers as string[] | undefined, workItemIds: args?.workItemIds as number[] | undefined, isDraft: args?.isDraft as boolean | undefined, }; const config loadAzureDevOpsConfig(); const git simpleGit(context.repoPath); const apiClient createApiClient(config); const result await executeAutoPR(context, git, apiClient, config); return { content: [ { type: text, text: formatResult(result), }, ], }; } case check_repo_status: { const repoPath (args?.repoPath as string) || process.cwd(); const status await getRepoStatus(repoPath); return { content: [ { type: text, text: JSON.stringify(status, null, 2), }, ], }; } default: throw new Error(Unknown tool: ${name}); } } catch (error) { const message error instanceof Error ? error.message : String(error); return { content: [ { type: text, text: 执行失败: ${message}, }, ], isError: true, }; } });错误处理的关键点MCP 工具调用失败时不要直接抛异常而是返回isError: true的内容。这样 AI 助手能理解发生了什么并可能尝试其他方案。如果直接抛异常AI 客户端可能只会显示一个通用的错误信息。4.3 配置管理PAT 不能硬编码Azure DevOps 的 PAT 绝对不能写在代码里。我的做法是通过环境变量读取interface AzureDevOpsConfig { organization: string; project: string; repository: string; pat: string; } function loadAzureDevOpsConfig(): AzureDevOpsConfig { const organization process.env.AZDO_ORG; const project process.env.AZDO_PROJECT; const repository process.env.AZDO_REPO; const pat process.env.AZDO_PAT; const missing: string[] []; if (!organization) missing.push(AZDO_ORG); if (!project) missing.push(AZDO_PROJECT); if (!repository) missing.push(AZDO_REPO); if (!pat) missing.push(AZDO_PAT); if (missing.length 0) { throw new Error( 缺少必要的环境变量: ${missing.join(, )}。请参考文档配置 Azure DevOps 连接信息。 ); } return { organization, project, repository, pat: pat! }; }在 MCP 客户端的配置文件里通过env字段传入这些环境变量。比如在 Claude Desktop 的配置里{ mcpServers: { auto-pr: { command: node, args: [/path/to/auto-pr-mcp/dist/index.js], env: { AZDO_ORG: your-org, AZDO_PROJECT: your-project, AZDO_REPO: your-repo, AZDO_PAT: your-pat } } } }安全提醒PAT 应该定期轮换并且只授予必要的权限。如果 PAT 泄露攻击者可以以你的身份操作仓库。我建议设置 90 天过期并且只勾选 Code (Read Write) 权限。5. 实操过程中踩过的坑与排查技巧5.1 Git 相关的典型问题问题一SSH 认证失败这是最常见的问题。git push报错Permission denied (publickey)说明 SSH key 没有配置好。排查步骤检查 SSH key 是否存在ls ~/.ssh/id_*检查 SSH agent 是否加载了 keyssh-add -l测试 SSH 连接ssh -T gitssh.dev.azure.com如果 SSH key 不存在需要生成ssh-keygen -t ed25519 -C your-email。然后把公钥添加到 Azure DevOps 的 SSH Keys 设置里。问题二分支名包含特殊字符Azure DevOps 对分支名有一些限制。比如分支名不能以/开头或结尾不能包含..、~、^、:等字符。如果分支名不合法创建 PR 时会报 400 错误。我的处理方式是在创建 PR 之前先校验分支名function validateBranchName(branchName: string): void { const invalidPatterns [ /^\./, /\.\./, /[~^:?*\[\\]/, /\/$/, /\.lock$/, /\{/, ]; for (const pattern of invalidPatterns) { if (pattern.test(branchName)) { throw new Error(分支名 ${branchName} 包含非法字符请重命名后再试); } } }问题三.gitignore不生效有时候明明在.gitignore里写了规则但git status还是显示那些文件。常见原因文件已经被跟踪了。.gitignore只对未跟踪的文件生效。如果文件已经被git add过需要先git rm --cached file。规则写法不对。比如*.log会匹配所有.log文件但/logs/只匹配根目录下的logs文件夹。全局.gitignore和项目.gitignore冲突。可以用git check-ignore -v file查看是哪条规则在生效。5.2 Azure DevOps API 的坑问题一PAT 权限不足创建 PR 需要 Code (Read Write) 权限。如果 PAT 只有 Code (Read)创建 PR 时会返回 403。排查方式是看错误信息里的message字段通常会提示具体缺少什么权限。问题二组织名和项目名混淆Azure DevOps 的 URL 格式是https://dev.azure.com/{organization}/{project}/_git/{repository}。很多人会把组织名和项目名搞混。组织名是你在 Azure DevOps 上的顶层命名空间项目名是组织下的具体项目。问题三API 版本不兼容不同版本的 Azure DevOps Serveron-premise支持的 API 版本不同。如果你用的是本地部署的 Azure DevOps Server可能需要降低 API 版本号。我建议先用7.1如果报错再尝试6.0或5.1。5.3 MCP 集成的问题问题一MCP Server 启动失败如果 MCP 客户端报“无法连接到 MCP Server”首先检查Node.js 版本是否 18入口文件路径是否正确是否有未捕获的异常导致进程退出可以在终端手动运行 MCP Server 来排查node dist/index.js。如果启动正常会看到进程挂起等待输入这是 stdio 传输的正常行为。问题二工具调用超时Azure DevOps API 的响应时间可能较长尤其是创建 PR 时。如果 MCP 客户端设置了较短的超时时间可能会中断调用。我的做法是在 axios 里设置 30 秒超时同时在 MCP 工具描述里提醒用户“创建 PR 可能需要几秒钟”。问题三环境变量不生效MCP 客户端启动 MCP Server 时环境变量是通过配置文件传入的。如果你在 shell 里export了变量但 MCP 客户端不是从 shell 启动的那些变量不会生效。确保在 MCP 客户端的配置里正确设置了env字段。5.4 常见问题速查表问题现象可能原因排查方法解决方案git push 报 Permission deniedSSH key 未配置ssh -T gitssh.dev.azure.com生成 SSH key 并添加到 Azure DevOps创建 PR 返回 401PAT 无效或过期检查 PAT 是否过期重新生成 PAT创建 PR 返回 403PAT 权限不足查看错误信息中的权限提示给 PAT 添加 Code (Read Write) 权限创建 PR 返回 400分支名不合法检查分支名是否包含特殊字符重命名分支MCP Server 无法启动Node.js 版本过低node --version升级到 Node.js 18工具调用无响应环境变量未配置检查 MCP 客户端配置在 env 字段中配置 AZDO_* 变量commit 后文件丢失detached HEAD 状态git status查看 HEAD 状态切换到正常分支后重新操作.gitignore 不生效文件已被跟踪git check-ignore -v filegit rm --cached file6. 一些只有实际跑过才知道的经验6.1 关于 commit message 的自动生成我最初想让 AI 根据 diff 内容自动生成 commit message但实际用下来发现效果不稳定。AI 有时候会生成过于笼统的描述比如“更新代码”有时候又会过度解读改动意图。后来我改成了“模板 可选覆盖”的策略默认使用auto: update from {branch}这样的模板用户可以通过commitMessage参数覆盖。这样既保证了自动化又保留了灵活性。如果你确实想让 AI 生成 commit message建议在 Skill 里加一个单独的步骤让 AI 先分析 diff 再生成 message而不是在创建 PR 的流程里顺便做。这样职责更清晰也更容易调试。6.2 关于 reviewer 的自动分配Azure DevOps 支持通过 API 指定 reviewer但需要用户的 ID通常是邮箱或 GUID。我试过根据文件路径自动分配 reviewer比如前端文件分配给前端负责人但维护这个映射关系很麻烦。目前我的做法是如果用户没有指定 reviewer就不自动添加。团队可以在 Azure DevOps 的项目设置里配置“自动包含的 reviewer”这样创建 PR 时会自动添加。这比在工具层面做映射要可靠得多。6.3 关于草稿 PR 的使用场景isDraft: true创建的 PR 不会被自动合并也不会触发一些 CI 检查。这个功能在以下场景很有用代码还没写完但想先让团队看到方向需要先跑一遍 CI 看看有没有问题但不想让 reviewer 现在就 review作为“占位符”防止其他人对同一分支创建 PR我通常会在迭代初期创建草稿 PR等代码稳定后再标记为 ready for review。6.4 关于错误恢复自动化工具有一个通病一旦中间步骤失败整个流程就卡住了。比如 commit 成功了但 push 失败了下次再运行工具时它会发现没有未提交的改动然后直接尝试 push。这其实是正确的行为但用户可能会困惑“为什么没有 commit 就直接 push 了”。我的做法是在返回结果里详细列出每一步的状态让用户清楚地知道哪些步骤执行了、哪些跳过了、哪些失败了。这样即使流程中断用户也能知道当前处于什么状态以及下一步该怎么做。6.5 关于性能优化这个工具的性能瓶颈主要在 Git 操作和 API 调用上。我做过一些优化并行化检查仓库状态和加载配置可以并行执行节省几百毫秒缓存Azure DevOps 的仓库信息比如默认分支可以缓存避免每次都查询懒加载simple-git 的实例不需要在启动时就创建等到实际使用时再创建但这些优化的收益有限因为整个流程的主要耗时在 push 和 API 调用上这些是网络 IO没法优化。所以我没有过度投入性能优化够用就行。6.6 关于测试策略MCP 工具的测试比较特殊因为它涉及外部依赖Git 仓库、Azure DevOps API。我的测试策略是单元测试mock 掉 simple-git 和 axios测试 Skill 编排逻辑集成测试用一个临时的 Git 仓库和 mock 的 Azure DevOps API Server测试完整的流程手动测试在真实的 Azure DevOps 仓库上测试验证端到端流程单元测试用 Vitest集成测试用 mswMock Service Worker来拦截 HTTP 请求。手动测试就是我自己日常使用发现问题就修。7. 这个工具还能怎么扩展目前这个工具只覆盖了“创建 PR”这一个场景但实际上还有很多可以扩展的方向。自动合并 PR当 PR 的 CI 检查通过且所有 reviewer 都 approve 后自动合并。这个需要监听 PR 的状态变化可以通过 Azure DevOps 的 Webhook 或者轮询 API 来实现。批量创建 PR如果你有多个仓库需要同时提交改动可以扩展成批量操作。每个仓库独立执行最后汇总结果。PR 模板支持Azure DevOps 支持 PR 模板可以在创建 PR 时自动填充描述。我的工具目前是手动传prDescription可以改成从模板文件读取。与工作项联动创建 PR 时自动关联工作项并在 PR 合并后自动更新工作项状态。这个需要 Work Items (Read Write) 权限。多平台支持目前只支持 Azure DevOps可以扩展到 GitHub、GitLab 等平台。MCP 的抽象层让这个扩展变得相对容易——只需要实现对应平台的 API 封装Skill 层的逻辑可以复用。我个人的优先级是先把手头的 Azure DevOps 场景做稳定再考虑扩展。毕竟工具的价值在于解决实际问题而不是支持尽可能多的平台。7.1 关于 Skill 的复用性思考这个项目让我对 Skill 的设计有了新的理解。一个好的 Skill 应该是原子性每个 Skill 只做一件事但做得很完整可组合多个 Skill 可以串联成更复杂的流程可观测每个步骤的状态和结果都清晰可见可恢复失败后可以从断点继续而不是从头再来我目前的设计基本满足前三点第四点可恢复还有改进空间。比如如果 push 失败了下次运行时会重新检查状态并继续但不会保留上次的上下文。如果要做到真正的可恢复需要把中间状态持久化到磁盘。这个可能是我下一步要优化的方向。不过说实话在实际使用中失败后重新运行一次的成本并不高所以优先级不高。7.2 关于 AI 助手集成的体验最后说一个有意思的观察当我把这个工具接入 AI 助手后使用方式发生了微妙的变化。以前我是“想好了要提 PR然后执行工具”现在我是“跟 AI 说我要提 PRAI 帮我判断当前状态并执行”。比如我会说“帮我把当前的改动提个 PR 到 develop标题写‘修复登录页面的样式问题’。”AI 会先调用check_repo_status看看当前分支和改动情况然后调用auto_create_pr完成提交。如果当前分支就是 developAI 会告诉我“你当前在 develop 分支上无法创建 PR需要先切换到 feature 分支”。这种交互方式比手动执行命令要自然得多。你不需要记住工具的具体参数只需要描述你的意图。这也是 MCP 协议的价值所在——它让 AI 助手真正成为了“能干活”的助手而不只是“能聊天”的助手。当然这也带来了新的挑战AI 可能会误解你的意图或者在不该调用工具的时候调用。所以工具的设计需要足够健壮能够处理各种边界情况并且在出错时给出清晰的提示。这也是我在实现过程中花最多时间的地方——不是写代码而是想清楚各种可能的失败场景并妥善处理。