create_pull_request_comment
Add comments to pull requests, including file-specific or line-specific feedback, and manage thread statuses for effective collaboration in Azure DevOps.
Instructions
Add a comment to a pull request
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| content | Yes | Comment content | |
| filePath | No | File path for file-specific comments (optional) | |
| lineNumber | No | Line number for line-specific comments (optional) | |
| pullRequestId | Yes | ID of the pull request | |
| status | No | Thread status (optional) | |
| threadId | No | Thread ID for replies (optional) |
Input Schema (JSON Schema)
{
"properties": {
"content": {
"description": "Comment content",
"type": "string"
},
"filePath": {
"description": "File path for file-specific comments (optional)",
"type": "string"
},
"lineNumber": {
"description": "Line number for line-specific comments (optional)",
"type": "number"
},
"pullRequestId": {
"description": "ID of the pull request",
"type": "number"
},
"status": {
"description": "Thread status (optional)",
"enum": [
"active",
"fixed",
"pending",
"wontfix",
"closed"
],
"type": "string"
},
"threadId": {
"description": "Thread ID for replies (optional)",
"type": "number"
}
},
"required": [
"pullRequestId",
"content"
],
"type": "object"
}
Implementation Reference
- src/tools/pullRequests.ts:244-460 (handler)Main handler function executing the tool: parses input with Zod schema, uses Azure DevOps Git client to create comments or threads, supports file/line-specific positioning by finding iterations and changes.export async function createPullRequestComment(rawParams: any) { // Parse arguments with defaults from environment variables const params = createPullRequestCommentSchema.parse({ pullRequestId: rawParams.pullRequestId, content: rawParams.content, threadId: rawParams.threadId, filePath: rawParams.filePath, lineNumber: rawParams.lineNumber, status: rawParams.status, }); console.error("[API] Creating PR comment:", params); try { // Get the Git API client const gitClient = await getGitClient(); if (params.threadId) { // Reply to an existing thread console.error(`[API] Adding comment to thread ${params.threadId}`); const comment = { content: params.content, parentCommentId: 0, // Root-level comment in thread }; const commentUrl = `${ORG_URL}/${DEFAULT_PROJECT}/_apis/git/repositories/${DEFAULT_REPOSITORY}/pullRequests/${params.pullRequestId}/threads/${params.threadId}/comments?api-version=7.0`; const result = await makeAzureDevOpsRequest(commentUrl, "POST", comment); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } else { // Create a new thread let thread: any = { comments: [ { content: params.content, commentType: 1, // 1 = text comment }, ], status: params.status || "active", }; // Handle file and line-specific comments if (params.filePath) { console.error( `[API] Creating code comment on file: ${params.filePath}` ); // Get iterations in ascending order const iterationsUrl = `${ORG_URL}/${DEFAULT_PROJECT}/_apis/git/repositories/${DEFAULT_REPOSITORY}/pullRequests/${params.pullRequestId}/iterations?api-version=7.1-preview.1`; const iterationsResponse = await makeAzureDevOpsRequest(iterationsUrl); if ( !iterationsResponse.value || iterationsResponse.value.length === 0 ) { throw new Error("No iterations found for pull request"); } // Normalize file path const normalizedPath = params.filePath.replace(/^\/+/, ""); console.error(`[API] Looking for file: ${normalizedPath}`); // Find the iteration where the file existed let targetIteration = null; let targetFileChange = null; for (const iteration of iterationsResponse.value) { console.error(`[API] Checking iteration ${iteration.id}`); const changesUrl = `${ORG_URL}/${DEFAULT_PROJECT}/_apis/git/repositories/${DEFAULT_REPOSITORY}/pullRequests/${params.pullRequestId}/iterations/${iteration.id}/changes?api-version=7.1-preview.1`; const changes = await makeAzureDevOpsRequest(changesUrl); console.error( `[API] Iteration ${iteration.id} changes:`, JSON.stringify( changes.changeEntries?.map((c: any) => c.item?.path), null, 2 ) ); // Try different path formats const pathVariations = [ normalizedPath, normalizedPath.replace(/^\/+/, ""), `/${normalizedPath}`, normalizedPath.toLowerCase(), normalizedPath.replace(/^\/+/, "").toLowerCase(), ]; console.error(`[API] Trying path variations:`, pathVariations); const fileChange = changes.changeEntries?.find( (change: { item?: { path?: string } }) => { const changePath = change.item?.path || ""; const match = pathVariations.some( (p) => changePath.toLowerCase() === p.toLowerCase() ); if (match) { console.error( `[API] Found match: ${changePath} matches ${pathVariations.find( (p) => changePath.toLowerCase() === p.toLowerCase() )}` ); } return match; } ); if (fileChange) { targetIteration = iteration; targetFileChange = fileChange; console.error( `[API] Found file in iteration ${iteration.id} with path ${fileChange.item.path}` ); break; } } if (!targetIteration || !targetFileChange) { throw new Error(`File ${normalizedPath} not found in any iteration`); } // Create thread with file position thread.threadContext = { filePath: normalizedPath, rightFileStart: { line: params.lineNumber, offset: 1, }, rightFileEnd: { line: params.lineNumber, offset: 1, }, }; // Set up the thread with version information const targetIterationId = Number(targetIteration.id); console.error( `[API] Using iteration ${targetIterationId} with change ID ${targetFileChange.changeTrackingId}` ); // Set thread properties for version control thread.properties = { "Microsoft.TeamFoundation.Discussion.SourceCommitId": { $type: "System.String", $value: targetFileChange.item.commitId, }, "Microsoft.TeamFoundation.Discussion.TargetCommitId": { $type: "System.String", $value: targetFileChange.item.commitId, }, "Microsoft.TeamFoundation.Discussion.Iteration": { $type: "System.String", $value: targetIterationId.toString(), }, }; // Set iteration context thread.pullRequestThreadContext = { iterationContext: { firstComparingIteration: targetIterationId, secondComparingIteration: targetIterationId, }, changeTrackingId: targetFileChange.changeTrackingId, }; thread.comments = [ { parentCommentId: 0, content: params.content, commentType: 1, }, ]; thread.status = "active"; console.error( "[API] Thread context:", JSON.stringify( { iteration: targetIteration.id, changeTracking: targetFileChange.changeTrackingId, }, null, 2 ) ); } else { console.error("[API] Creating general PR comment"); } // Create the new thread const threadUrl = `${ORG_URL}/${DEFAULT_PROJECT}/_apis/git/repositories/${DEFAULT_REPOSITORY}/pullRequests/${params.pullRequestId}/threads?api-version=7.0`; const result = await makeAzureDevOpsRequest(threadUrl, "POST", thread); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } } catch (error) { logError("Error creating PR comment", error); throw error; } }
- src/schemas/pullRequests.ts:42-55 (schema)Zod schema for validating input parameters to the createPullRequestComment handler.export const createPullRequestCommentSchema = z.object({ pullRequestId: z.number(), content: z.string(), threadId: z.number().optional(), // For replying to existing threads filePath: z.string().optional(), // For file-specific comments lineNumber: z.number().optional(), // For line-specific comments status: z .enum(["active", "fixed", "pending", "wontfix", "closed"]) .optional(), // Thread status }); export type CreatePullRequestCommentParams = z.infer< typeof createPullRequestCommentSchema >;
- src/tools/pullRequests.ts:924-958 (registration)Tool registration definition within pullRequestTools array, including name, description, and inputSchema; exported and included in server's tool list.{ name: "create_pull_request_comment", description: "Add a comment to a pull request", inputSchema: { type: "object", properties: { pullRequestId: { type: "number", description: "ID of the pull request", }, content: { type: "string", description: "Comment content", }, threadId: { type: "number", description: "Thread ID for replies (optional)", }, filePath: { type: "string", description: "File path for file-specific comments (optional)", }, lineNumber: { type: "number", description: "Line number for line-specific comments (optional)", }, status: { type: "string", enum: ["active", "fixed", "pending", "wontfix", "closed"], description: "Thread status (optional)", }, }, required: ["pullRequestId", "content"], }, },
- src/index.ts:83-84 (registration)Dispatcher switch case in main MCP server handler that routes calls to the createPullRequestComment function.case "create_pull_request_comment": return await createPullRequestComment(request.params.arguments || {});
- src/index.ts:24-31 (registration)Import of the handler function and tools array from pullRequests module into main index.listPullRequests, getPullRequest, createPullRequest, createPullRequestComment, getPullRequestDiff, updatePullRequest, // Added import pullRequestTools, } from "./tools/pullRequests.js";