Skip to content

Commit d5dabf6

Browse files
authored
List PRs created by specific user (Issue 424) (#437)
List PRs created by specific user. Also refactoring some code (moving some user related util functions to `auth.ts`), and adding auth-related tests. ## GitHub issue number Fixes #424 ## **Associated Risks** / ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Added tests and executed them. Manually tested the tool.
1 parent 97ad00f commit d5dabf6

File tree

4 files changed

+416
-32
lines changed

4 files changed

+416
-32
lines changed

src/tools/auth.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
import { AccessToken } from "@azure/identity";
55
import { WebApi } from "azure-devops-node-api";
6+
import { apiVersion } from "../utils.js";
7+
import { IdentityBase } from "azure-devops-node-api/interfaces/IdentitiesInterfaces.js";
8+
9+
interface IdentitiesResponse {
10+
value: IdentityBase[];
11+
}
612

713
async function getCurrentUserDetails(tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string) {
814
const connection = await connectionProvider();
@@ -23,4 +29,53 @@ async function getCurrentUserDetails(tokenProvider: () => Promise<AccessToken>,
2329
return data;
2430
}
2531

26-
export { getCurrentUserDetails };
32+
/**
33+
* Searches for identities using Azure DevOps Identity API
34+
*/
35+
async function searchIdentities(identity: string, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string): Promise<IdentitiesResponse> {
36+
const token = await tokenProvider();
37+
const connection = await connectionProvider();
38+
const orgName = connection.serverUrl.split("/")[3];
39+
const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
40+
41+
const params = new URLSearchParams({
42+
"api-version": apiVersion,
43+
"searchFilter": "General",
44+
"filterValue": identity,
45+
});
46+
47+
const response = await fetch(`${baseUrl}?${params}`, {
48+
headers: {
49+
"Authorization": `Bearer ${token.token}`,
50+
"Content-Type": "application/json",
51+
"User-Agent": userAgentProvider(),
52+
},
53+
});
54+
55+
if (!response.ok) {
56+
const errorText = await response.text();
57+
throw new Error(`HTTP ${response.status}: ${errorText}`);
58+
}
59+
60+
return await response.json();
61+
}
62+
63+
/**
64+
* Gets the user ID from email or unique name using Azure DevOps Identity API
65+
*/
66+
async function getUserIdFromEmail(userEmail: string, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string): Promise<string> {
67+
const identities = await searchIdentities(userEmail, tokenProvider, connectionProvider, userAgentProvider);
68+
69+
if (!identities || identities.value?.length === 0) {
70+
throw new Error(`No user found with email/unique name: ${userEmail}`);
71+
}
72+
73+
const firstIdentity = identities.value[0];
74+
if (!firstIdentity.id) {
75+
throw new Error(`No ID found for user with email/unique name: ${userEmail}`);
76+
}
77+
78+
return firstIdentity.id;
79+
}
80+
81+
export { getCurrentUserDetails, getUserIdFromEmail, searchIdentities };

src/tools/core.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AccessToken } from "@azure/identity";
55
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
66
import { WebApi } from "azure-devops-node-api";
77
import { z } from "zod";
8-
import { apiVersion } from "../utils.js";
8+
import { searchIdentities } from "./auth.js";
99

1010
import type { ProjectInfo } from "azure-devops-node-api/interfaces/CoreInterfaces.js";
1111
import { IdentityBase } from "azure-devops-node-api/interfaces/IdentitiesInterfaces.js";
@@ -99,31 +99,7 @@ function configureCoreTools(server: McpServer, tokenProvider: () => Promise<Acce
9999
},
100100
async ({ searchFilter }) => {
101101
try {
102-
const token = await tokenProvider();
103-
const connection = await connectionProvider();
104-
const orgName = connection.serverUrl.split("/")[3];
105-
const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
106-
107-
const params = new URLSearchParams({
108-
"api-version": apiVersion,
109-
"searchFilter": "General",
110-
"filterValue": searchFilter,
111-
});
112-
113-
const response = await fetch(`${baseUrl}?${params}`, {
114-
headers: {
115-
"Authorization": `Bearer ${token.token}`,
116-
"Content-Type": "application/json",
117-
"User-Agent": userAgentProvider(),
118-
},
119-
});
120-
121-
if (!response.ok) {
122-
const errorText = await response.text();
123-
throw new Error(`HTTP ${response.status}: ${errorText}`);
124-
}
125-
126-
const identities = await response.json();
102+
const identities = await searchIdentities(searchFilter, tokenProvider, connectionProvider, userAgentProvider);
127103

128104
if (!identities || identities.value?.length === 0) {
129105
return { content: [{ type: "text", text: "No identities found" }], isError: true };

src/tools/repos.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
CommentThreadStatus,
1919
} from "azure-devops-node-api/interfaces/GitInterfaces.js";
2020
import { z } from "zod";
21-
import { getCurrentUserDetails } from "./auth.js";
21+
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
2222
import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces.js";
2323
import { getEnumKeys } from "../utils.js";
2424

@@ -270,13 +270,14 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<Acce
270270
top: z.number().default(100).describe("The maximum number of pull requests to return."),
271271
skip: z.number().default(0).describe("The number of pull requests to skip."),
272272
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
273+
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."),
273274
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
274275
status: z
275276
.enum(getEnumKeys(PullRequestStatus) as [string, ...string[]])
276277
.default("Active")
277278
.describe("Filter pull requests by status. Defaults to 'Active'."),
278279
},
279-
async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
280+
async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
280281
const connection = await connectionProvider();
281282
const gitApi = await connection.getGitApi();
282283

@@ -291,7 +292,22 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<Acce
291292
repositoryId: repositoryId,
292293
};
293294

294-
if (created_by_me || i_am_reviewer) {
295+
if (created_by_user) {
296+
try {
297+
const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
298+
searchCriteria.creatorId = userId;
299+
} catch (error) {
300+
return {
301+
content: [
302+
{
303+
type: "text",
304+
text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
305+
},
306+
],
307+
isError: true,
308+
};
309+
}
310+
} else if (created_by_me || i_am_reviewer) {
295311
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
296312
const userId = data.authenticatedUser.id;
297313
if (created_by_me) {
@@ -341,13 +357,14 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<Acce
341357
top: z.number().default(100).describe("The maximum number of pull requests to return."),
342358
skip: z.number().default(0).describe("The number of pull requests to skip."),
343359
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
360+
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."),
344361
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
345362
status: z
346363
.enum(getEnumKeys(PullRequestStatus) as [string, ...string[]])
347364
.default("Active")
348365
.describe("Filter pull requests by status. Defaults to 'Active'."),
349366
},
350-
async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
367+
async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
351368
const connection = await connectionProvider();
352369
const gitApi = await connection.getGitApi();
353370

@@ -360,7 +377,22 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<Acce
360377
status: pullRequestStatusStringToInt(status),
361378
};
362379

363-
if (created_by_me || i_am_reviewer) {
380+
if (created_by_user) {
381+
try {
382+
const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
383+
gitPullRequestSearchCriteria.creatorId = userId;
384+
} catch (error) {
385+
return {
386+
content: [
387+
{
388+
type: "text",
389+
text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
390+
},
391+
],
392+
isError: true,
393+
};
394+
}
395+
} else if (created_by_me || i_am_reviewer) {
364396
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
365397
const userId = data.authenticatedUser.id;
366398
if (created_by_me) {

0 commit comments

Comments
 (0)