tools/mcp/dotnet/AzureSDKDevToolsMCP/Tools/SpecPullRequestTool.cs (140 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.ComponentModel;
using System.Text.Json;
using AzureSDKDevToolsMCP.Helpers;
using AzureSDKDevToolsMCP.Services;
using AzureSDKDSpecTools.Models;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using Octokit;
namespace AzureSDKDevToolsMCP.Tools
{
[Description("TypeSpec pull request tools")]
[McpServerToolType]
public class SpecPullRequestTool(IGitHubService _service, IGitHelper _helper, ILogger<SpecPullRequestTool> _logger)
{
private readonly IGitHubService gitHubService = _service;
private readonly IGitHelper gitHelper = _helper;
private readonly ILogger<SpecPullRequestTool> logger = _logger;
readonly string TEST_IGNORE_TAG = "[TEST-IGNORE]";
[McpServerTool, Description("Connect to GitHub using personal access token.")]
public async Task<string> GetGitHubUserDetails()
{
var user = await this.gitHubService.GetGitUserDetails();
return user != null
? $"Connected to GitHub as {user.Login}"
: "Failed to connect to GitHub. Please make sure to login to GitHub using gh auth login to connect to GitHub.";
}
[McpServerTool, Description("Get Pull Request Status: Get TypeSpec pull request status for a given pull request number.")]
public async Task<string> GetPullRequestStatus(int pullRequestNumber, string typeSpecProjectPath, string repoOwner = "Azure")
{
try
{
logger.LogInformation($"Getting pull request details for {pullRequestNumber}...");
logger.LogInformation($"Repo owner: {repoOwner}");
var repoPath = gitHelper.GetRepoRootPath(typeSpecProjectPath);
logger.LogInformation($"Repo path: {repoPath}");
var repoName = gitHelper.GetRepoName(repoPath);
logger.LogInformation($"Repo name: {repoName}");
var pullRequest = await gitHubService.GetPullRequestAsync(repoOwner, repoName, pullRequestNumber);
var prStatus = pullRequest.State == ItemState.Open ? "Open" : "Closed";
var mergeStatus = pullRequest.Merged ? "Merged" : "Not Merged";
if (pullRequest.State == ItemState.Open)
{
mergeStatus = pullRequest.Mergeable == true ? "Open (PR is Mergeable)" : "Open (PR is not ready to merge)";
}
return JsonSerializer.Serialize(pullRequest);
}
catch (Exception ex)
{
return $"Failed to get pull request details: {ex.Message}";
}
}
[McpServerTool, Description("Get TypeSpec pull request checks for a given pull request number and TypeSpec project path. This tool calls Get Pull Request tool.")]
public async Task<List<String>> GetPullRequestChecks(int pullRequestNumber, string typeSpecProjectPath, string repoOwner = "Azure")
{
var repoPath = gitHelper.GetRepoRootPath(typeSpecProjectPath);
var repoName = gitHelper.GetRepoName(repoPath);
var checkResults = new List<string>();
var pullRequest = await GetPullRequestStatus(pullRequestNumber, repoPath);
if (pullRequest != null)
{
var checks = await gitHubService.GetPullRequestChecksAsync(repoOwner, repoName, pullRequestNumber);
foreach (var check in checks)
{
checkResults.Add($"Name: {check.Name}, Ignore failure: {check.Name.Contains(TEST_IGNORE_TAG)}, Status: {check.Status}, Output: {check.Output}, Conclusion: {check.Conclusion}, Link: {check.HtmlUrl}");
}
}
if (checkResults.Count == 0)
{
checkResults.Add("No checks found for the pull request.");
}
else
{
checkResults.Add($"Total checks found: {checkResults.Count}");
checkResults.Add($"Total checks ignored: {checkResults.Count(check => check.Contains(TEST_IGNORE_TAG))}. Any failures for ignorable check can be ignored.");
}
return checkResults;
}
[McpServerTool, Description("Check if TypeSpec project is in public repo.")]
public bool CheckIfSpecInPublicRepo(string typeSpecProjectPath)
{
var repoRootPath = gitHelper.GetRepoRootPath(typeSpecProjectPath);
var isPublicRepo = gitHelper.IsRepoPathForPublicSpecRepo(repoRootPath);
return isPublicRepo;
}
[McpServerTool, Description("Get pull request for current branch")]
public async Task<string> GetPullRequestForCurrentBranch(string typeSpecProjectPath, string repoOwner = "Azure")
{
var repoRootPath = gitHelper.GetRepoRootPath(typeSpecProjectPath);
if (string.IsNullOrEmpty(repoRootPath))
{
return "Failed to get repo root path. Please make sure to select the TypeSpec project path.";
}
var repoName = gitHelper.GetRepoName(repoRootPath);
var headRepoOwner = await gitHelper.GetRepoOwnerName(repoRootPath, false);
var headBranchName = gitHelper.GetBranchName(repoRootPath);
var headBranchRef = $"{headRepoOwner}:{headBranchName}";
logger.LogInformation($"Repo name: {repoName}, Head repo owner: {headRepoOwner}, Head branch name: {headBranchName}, Head branch ref: {headBranchRef}");
if (string.IsNullOrEmpty(headBranchName) || string.IsNullOrEmpty(repoName) || string.IsNullOrEmpty(headRepoOwner))
{
return "Failed to get repo details. Please make sure to select the TypeSpec project path and try again.";
}
logger.LogInformation($"Getting pull request for branch {headBranchRef}...");
var pullRequest = await gitHubService.GetPullRequestForBranchAsync(repoOwner, repoName, headBranchRef);
return pullRequest != null
? $"Pull request found: {pullRequest.HtmlUrl}"
: "No pull request found for the current branch.";
}
[McpServerTool, Description("Create pull request for spec changes. Creates a pull request for committed changes in the current branch.")]
public async Task<List<string>> CreatePullRequest(string title, string description, string typeSpecProjectPath, string targetBranch = "main", string targetRepoOwner = "Azure")
{
List<string> results = new();
//Get head branch name
var repoRootPath = gitHelper.GetRepoRootPath(typeSpecProjectPath);
var headBranchName = gitHelper.GetBranchName(repoRootPath);
if (string.IsNullOrEmpty(headBranchName) || headBranchName.Equals("main"))
{
results.Add("Failed to create pull request. Pull request can not be created for changes in main branch. Select the GitHub branch for your spec changes using `git checkout <branch name>'");
return results;
}
//Get repo details like target owner, head owner, repo name
var repoName = gitHelper.GetRepoName(repoRootPath);
var headRepoOwner = await gitHelper.GetRepoOwnerName(repoRootPath, false);
var headBranch = $"{headRepoOwner}:{headBranchName}";
logger.LogInformation($"Repo name: {repoName}, Head repo owner: {headRepoOwner}, Head branch name: {headBranchName}, Head branch ref: {headBranch}");
logger.LogInformation($"Creating pull request in {targetRepoOwner}:{repoName}");
//Create pull request
var createResponseList = await gitHubService.CreatePullRequest(repoName, targetRepoOwner, targetBranch, headBranch, title, description);
results.AddRange(createResponseList);
return results;
}
[McpServerTool, Description("Get pull request comments.")]
public async Task<List<string>> GetPullRequestComments(int pullRequestNumber, string repoName, string repoOwner = "Azure")
{
var comments = await gitHubService.GetPullRequestCommentsAsync(repoOwner, repoName, pullRequestNumber);
if (comments == null || comments.Count == 0)
{
return new List<string>() { "No comments found for the pull request." };
}
return comments;
}
}
}