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: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ Interact with these Azure DevOps services:
### 📁 Repositories

- **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
- **repo_list_pull_requests_by_repo**: Retrieve a list of pull requests for a given repository.
- **repo_list_pull_requests_by_project**: Retrieve a list of pull requests for a given project ID or name.
- **repo_list_pull_requests_by_repo_or_project**: Retrieve a list of pull requests for a given repository or project.
- **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
- **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
- **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
Expand Down
173 changes: 55 additions & 118 deletions src/tools/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GitPullRequestMergeStrategy,
GitPullRequest,
GitPullRequestCommentThread,
Comment,
} from "azure-devops-node-api/interfaces/GitInterfaces.js";
import { z } from "zod";
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
Expand All @@ -27,8 +28,7 @@ import { getEnumKeys } from "../utils.js";

const REPO_TOOLS = {
list_repos_by_project: "repo_list_repos_by_project",
list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
list_pull_requests_by_project: "repo_list_pull_requests_by_project",
list_pull_requests_by_repo_or_project: "repo_list_pull_requests_by_repo_or_project",
list_branches_by_repo: "repo_list_branches_by_repo",
list_my_branches_by_repo: "repo_list_my_branches_by_repo",
list_pull_request_threads: "repo_list_pull_request_threads",
Expand Down Expand Up @@ -71,7 +71,7 @@ function trimPullRequestThread(thread: GitPullRequestCommentThread) {
* @param comments Array of comments to trim (can be undefined/null)
* @returns Array of trimmed comment objects with essential properties only
*/
function trimComments(comments: any[] | undefined | null) {
function trimComments(comments: Comment[] | undefined | null) {
return comments
?.filter((comment) => !comment.isDeleted) // Exclude deleted comments
?.map((comment) => ({
Expand Down Expand Up @@ -431,10 +431,11 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
);

server.tool(
REPO_TOOLS.list_pull_requests_by_repo,
"Retrieve a list of pull requests for a given repository.",
REPO_TOOLS.list_pull_requests_by_repo_or_project,
"Retrieve a list of pull requests for a given repository. Either repositoryId or project must be provided.",
{
repositoryId: z.string().describe("The ID of the repository where the pull requests are located."),
repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
project: z.string().optional().describe("The ID of the project where the pull requests are located."),
top: z.number().default(100).describe("The maximum number of pull requests to return."),
skip: z.number().default(0).describe("The number of pull requests to skip."),
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
Expand All @@ -451,23 +452,38 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
},
async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
async ({ repositoryId, project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
const connection = await connectionProvider();
const gitApi = await connection.getGitApi();

// Build the search criteria
const searchCriteria: {
status: number;
repositoryId: string;
repositoryId?: string;
creatorId?: string;
reviewerId?: string;
sourceRefName?: string;
targetRefName?: string;
} = {
status: pullRequestStatusStringToInt(status),
repositoryId: repositoryId,
};

if (!repositoryId && !project) {
return {
content: [
{
type: "text",
text: "Either repositoryId or project must be provided.",
},
],
isError: true,
};
}

if (repositoryId) {
searchCriteria.repositoryId = repositoryId;
}

if (sourceRefName) {
searchCriteria.sourceRefName = sourceRefName;
}
Expand Down Expand Up @@ -518,117 +534,38 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
searchCriteria.reviewerId = userId;
}

const pullRequests = await gitApi.getPullRequests(
repositoryId,
searchCriteria,
undefined, // project
undefined, // maxCommentLength
skip,
top
);

const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));

return {
content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
};
}
);

server.tool(
REPO_TOOLS.list_pull_requests_by_project,
"Retrieve a list of pull requests for a given project Id or Name.",
{
project: z.string().describe("The name or ID of the Azure DevOps project."),
top: z.number().default(100).describe("The maximum number of pull requests to return."),
skip: z.number().default(0).describe("The number of pull requests to skip."),
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
user_is_reviewer: z
.string()
.optional()
.describe("Filter pull requests where a specific user is a reviewer (provide email or unique name). Takes precedence over i_am_reviewer if both are provided."),
status: z
.enum(getEnumKeys(PullRequestStatus) as [string, ...string[]])
.default("Active")
.describe("Filter pull requests by status. Defaults to 'Active'."),
sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
},
async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
const connection = await connectionProvider();
const gitApi = await connection.getGitApi();

// Build the search criteria
const gitPullRequestSearchCriteria: {
status: number;
creatorId?: string;
reviewerId?: string;
sourceRefName?: string;
targetRefName?: string;
} = {
status: pullRequestStatusStringToInt(status),
};

if (sourceRefName) {
gitPullRequestSearchCriteria.sourceRefName = sourceRefName;
}

if (targetRefName) {
gitPullRequestSearchCriteria.targetRefName = targetRefName;
}

if (created_by_user) {
try {
const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
gitPullRequestSearchCriteria.creatorId = userId;
} catch (error) {
return {
content: [
{
type: "text",
text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
} else if (created_by_me) {
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
const userId = data.authenticatedUser.id;
gitPullRequestSearchCriteria.creatorId = userId;
}

if (user_is_reviewer) {
try {
const reviewerUserId = await getUserIdFromEmail(user_is_reviewer, tokenProvider, connectionProvider, userAgentProvider);
gitPullRequestSearchCriteria.reviewerId = reviewerUserId;
} catch (error) {
return {
content: [
{
type: "text",
text: `Error finding reviewer with email ${user_is_reviewer}: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
} else if (i_am_reviewer) {
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
const userId = data.authenticatedUser.id;
gitPullRequestSearchCriteria.reviewerId = userId;
let pullRequests;
if (repositoryId) {
pullRequests = await gitApi.getPullRequests(
repositoryId,
searchCriteria,
project, // project
undefined, // maxCommentLength
skip,
top
);
} else if (project) {
// If only project is provided, use getPullRequestsByProject
pullRequests = await gitApi.getPullRequestsByProject(
project,
searchCriteria,
undefined, // maxCommentLength
skip,
top
);
} else {
// This case should not occur due to earlier validation, but added for completeness
return {
content: [
{
type: "text",
text: "Either repositoryId or project must be provided.",
},
],
isError: true,
};
}

const pullRequests = await gitApi.getPullRequestsByProject(
project,
gitPullRequestSearchCriteria,
undefined, // maxCommentLength
skip,
top
);

const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));

return {
Expand Down
Loading