Skip to main content
Glama
mmruesch12
by mmruesch12

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

TableJSON Schema
NameRequiredDescriptionDefault
contentYesComment content
filePathNoFile path for file-specific comments (optional)
lineNumberNoLine number for line-specific comments (optional)
pullRequestIdYesID of the pull request
statusNoThread status (optional)
threadIdNoThread ID for replies (optional)

Implementation Reference

  • 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;
      }
    }
  • 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
    >;
  • 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";

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mmruesch12/azdo-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server