Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ This project implements an MCP server that exposes tools for querying and managi
| `search_pull_requests` | Searches pull requests by text in title or description |
| `query_pull_requests` | Advanced query with multiple combined filters |
| `create_pull_request` | Creates a new pull request with title, description, source/target branches, draft flag, reviewers, and linked work items |
| `update_pull_request` | Updates an existing pull request title, description, target branch, status, or draft flag |

### Pipeline/Build Tools

Expand Down Expand Up @@ -398,6 +399,8 @@ After configuring the MCP client, you can ask questions like:
- "Create a pull request from 'feature/login' to 'main' titled 'Add login page'"
- "Open a draft PR from my branch to main with a description of the changes"
- "Create a PR and link it to work items #123 and #456"
- "Update PR #456 to change the title and mark it ready for review"
- "Abandon PR #456 in the 'my-repo' repository"

### Pipelines and Builds

Expand Down
82 changes: 82 additions & 0 deletions src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,74 @@ public async Task<PullRequestDto> CreatePullRequestAsync(
}
}

public async Task<PullRequestDto> UpdatePullRequestAsync(
string repositoryNameOrId,
int pullRequestId,
string? title = null,
string? description = null,
string? targetRefName = null,
string? status = null,
bool? isDraft = null,
string? project = null,
CancellationToken cancellationToken = default)
{
try
{
var projectName = project ?? _options.DefaultProject;
_logger.LogDebug(
"Updating pull request {PullRequestId} in repository {Repository}",
pullRequestId,
repositoryNameOrId);

var pullRequestUpdate = new GitPullRequest();

if (title is not null)
{
pullRequestUpdate.Title = title;
}

if (description is not null)
{
pullRequestUpdate.Description = description;
}

if (targetRefName is not null)
{
pullRequestUpdate.TargetRefName = targetRefName;
}

if (status is not null)
{
pullRequestUpdate.Status = ParsePullRequestUpdateStatus(status)
?? throw new ArgumentException($"Unsupported pull request status '{status}'", nameof(status));
}

if (isDraft.HasValue)
{
pullRequestUpdate.IsDraft = isDraft.Value;
}

var result = await _gitClient.UpdatePullRequestAsync(
gitPullRequestToUpdate: pullRequestUpdate,
project: projectName,
repositoryId: repositoryNameOrId,
pullRequestId: pullRequestId,
userState: null,
cancellationToken: cancellationToken);

return MapToPullRequestDto(result);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error updating pull request {PullRequestId} in repository {Repository}",
pullRequestId,
repositoryNameOrId);
throw;
}
}

private static PullRequestStatus? ParsePullRequestStatus(string? status)
{
if (string.IsNullOrEmpty(status))
Expand All @@ -1772,6 +1840,20 @@ public async Task<PullRequestDto> CreatePullRequestAsync(
};
}

private static PullRequestStatus? ParsePullRequestUpdateStatus(string? status)
{
if (string.IsNullOrWhiteSpace(status))
return null;

return status.Trim().ToLowerInvariant().Replace("_", string.Empty).Replace("-", string.Empty) switch
{
"active" or "open" or "reopen" or "reactivate" or "reactivated" => PullRequestStatus.Active,
"abandoned" or "abandon" => PullRequestStatus.Abandoned,
"completed" or "complete" or "merge" or "merged" => PullRequestStatus.Completed,
_ => null
};
}

private static PullRequestDto MapToPullRequestDto(GitPullRequest pr)
{
return new PullRequestDto
Expand Down
24 changes: 24 additions & 0 deletions src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,30 @@ Task<PullRequestDto> CreatePullRequestAsync(
IEnumerable<int>? workItemIds = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Updates an existing pull request in the specified repository.
/// </summary>
/// <param name="repositoryNameOrId">The repository name or ID.</param>
/// <param name="pullRequestId">The pull request ID.</param>
/// <param name="title">Optional new pull request title.</param>
/// <param name="description">Optional new pull request description.</param>
/// <param name="targetRefName">Optional new target branch (e.g., refs/heads/main).</param>
/// <param name="status">Optional new status: active, abandoned, or completed.</param>
/// <param name="isDraft">Optional draft flag.</param>
/// <param name="project">The project name (optional if default project is configured).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The updated pull request.</returns>
Task<PullRequestDto> UpdatePullRequestAsync(
string repositoryNameOrId,
int pullRequestId,
string? title = null,
string? description = null,
string? targetRefName = null,
string? status = null,
bool? isDraft = null,
string? project = null,
CancellationToken cancellationToken = default);

#endregion

#region Pipeline/Build Operations
Expand Down
84 changes: 84 additions & 0 deletions src/Viamus.Azure.Devops.Mcp.Server/Tools/PullRequestTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,76 @@ public async Task<string> CreatePullRequest(
}, JsonOptions);
}

[McpServerTool(Name = "update_pull_request")]
[Description("Updates an existing pull request. Only specified fields are changed; omitted fields remain unchanged. Supports title, description, target branch, status, and draft flag.")]
public async Task<string> UpdatePullRequest(
[Description("The repository name or ID")] string repositoryNameOrId,
[Description("The pull request ID")] int pullRequestId,
[Description("New pull request title")] string? title = null,
[Description("New pull request description")] string? description = null,
[Description("New target branch (e.g., 'refs/heads/main')")] string? targetRefName = null,
[Description("New status: Active, Abandoned, or Completed. Aliases include open, reopen, abandon, complete, and merge.")] string? status = null,
[Description("New draft flag. Use true to mark as draft, false to mark ready for review.")] bool? isDraft = null,
[Description("The project name (optional if default project is configured)")] string? project = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(repositoryNameOrId))
{
return JsonSerializer.Serialize(new { error = "Repository name or ID is required" }, JsonOptions);
}

if (pullRequestId <= 0)
{
return JsonSerializer.Serialize(new { error = "Pull request ID must be a positive integer" }, JsonOptions);
}

if (title is not null && string.IsNullOrWhiteSpace(title))
{
return JsonSerializer.Serialize(new { error = "Title cannot be empty" }, JsonOptions);
}

if (targetRefName is not null && string.IsNullOrWhiteSpace(targetRefName))
{
return JsonSerializer.Serialize(new { error = "Target branch cannot be empty" }, JsonOptions);
}

var normalizedStatus = status is null ? null : NormalizePullRequestStatus(status);
if (status is not null && normalizedStatus is null)
{
return JsonSerializer.Serialize(new
{
error = "Unsupported pull request status. Supported statuses are Active, Abandoned, and Completed. Aliases include open, reopen, abandon, complete, and merge."
}, JsonOptions);
}

if (title is null &&
description is null &&
targetRefName is null &&
normalizedStatus is null &&
isDraft is null)
{
return JsonSerializer.Serialize(new { error = "At least one pull request field must be provided" }, JsonOptions);
}

var pullRequest = await _azureDevOpsService.UpdatePullRequestAsync(
repositoryNameOrId,
pullRequestId,
title,
description,
targetRefName,
normalizedStatus,
isDraft,
project,
cancellationToken);

return JsonSerializer.Serialize(new
{
success = true,
message = $"Pull request {pullRequest.PullRequestId} updated successfully",
pullRequest
}, JsonOptions);
}

[McpServerTool(Name = "query_pull_requests")]
[Description("Advanced query for pull requests with multiple combined filters. Allows filtering by status, branches, dates, creator, and reviewer simultaneously.")]
public async Task<string> QueryPullRequests(
Expand Down Expand Up @@ -424,4 +494,18 @@ public async Task<string> QueryPullRequests(
pullRequests
}, JsonOptions);
}

private static string? NormalizePullRequestStatus(string? status)
{
if (string.IsNullOrWhiteSpace(status))
return null;

return status.Trim().ToLowerInvariant().Replace("_", string.Empty).Replace("-", string.Empty) switch
{
"active" or "open" or "reopen" or "reactivate" or "reactivated" => "Active",
"abandoned" or "abandon" => "Abandoned",
"completed" or "complete" or "merge" or "merged" => "Completed",
_ => null
};
}
}
Loading
Loading