@azure-devops/mcp 1.3.1-nightly.20250821 → 1.3.1-nightly.20250822

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { apiVersion } from "../utils.js";
3
4
  async function getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider) {
4
5
  const connection = await connectionProvider();
5
6
  const url = `${connection.serverUrl}/_apis/connectionData`;
@@ -18,4 +19,44 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider, userAgen
18
19
  }
19
20
  return data;
20
21
  }
21
- export { getCurrentUserDetails };
22
+ /**
23
+ * Searches for identities using Azure DevOps Identity API
24
+ */
25
+ async function searchIdentities(identity, tokenProvider, connectionProvider, userAgentProvider) {
26
+ const token = await tokenProvider();
27
+ const connection = await connectionProvider();
28
+ const orgName = connection.serverUrl.split("/")[3];
29
+ const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
30
+ const params = new URLSearchParams({
31
+ "api-version": apiVersion,
32
+ "searchFilter": "General",
33
+ "filterValue": identity,
34
+ });
35
+ const response = await fetch(`${baseUrl}?${params}`, {
36
+ headers: {
37
+ "Authorization": `Bearer ${token.token}`,
38
+ "Content-Type": "application/json",
39
+ "User-Agent": userAgentProvider(),
40
+ },
41
+ });
42
+ if (!response.ok) {
43
+ const errorText = await response.text();
44
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+ /**
49
+ * Gets the user ID from email or unique name using Azure DevOps Identity API
50
+ */
51
+ async function getUserIdFromEmail(userEmail, tokenProvider, connectionProvider, userAgentProvider) {
52
+ const identities = await searchIdentities(userEmail, tokenProvider, connectionProvider, userAgentProvider);
53
+ if (!identities || identities.value?.length === 0) {
54
+ throw new Error(`No user found with email/unique name: ${userEmail}`);
55
+ }
56
+ const firstIdentity = identities.value[0];
57
+ if (!firstIdentity.id) {
58
+ throw new Error(`No ID found for user with email/unique name: ${userEmail}`);
59
+ }
60
+ return firstIdentity.id;
61
+ }
62
+ export { getCurrentUserDetails, getUserIdFromEmail, searchIdentities };
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
- import { apiVersion } from "../utils.js";
4
+ import { searchIdentities } from "./auth.js";
5
5
  const CORE_TOOLS = {
6
6
  list_project_teams: "core_list_project_teams",
7
7
  list_projects: "core_list_projects",
@@ -68,27 +68,7 @@ function configureCoreTools(server, tokenProvider, connectionProvider, userAgent
68
68
  searchFilter: z.string().describe("Search filter (unique namme, display name, email) to retrieve identity IDs for."),
69
69
  }, async ({ searchFilter }) => {
70
70
  try {
71
- const token = await tokenProvider();
72
- const connection = await connectionProvider();
73
- const orgName = connection.serverUrl.split("/")[3];
74
- const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
75
- const params = new URLSearchParams({
76
- "api-version": apiVersion,
77
- "searchFilter": "General",
78
- "filterValue": searchFilter,
79
- });
80
- const response = await fetch(`${baseUrl}?${params}`, {
81
- headers: {
82
- "Authorization": `Bearer ${token.token}`,
83
- "Content-Type": "application/json",
84
- "User-Agent": userAgentProvider(),
85
- },
86
- });
87
- if (!response.ok) {
88
- const errorText = await response.text();
89
- throw new Error(`HTTP ${response.status}: ${errorText}`);
90
- }
91
- const identities = await response.json();
71
+ const identities = await searchIdentities(searchFilter, tokenProvider, connectionProvider, userAgentProvider);
92
72
  if (!identities || identities.value?.length === 0) {
93
73
  return { content: [{ type: "text", text: "No identities found" }], isError: true };
94
74
  }
@@ -2,7 +2,7 @@
2
2
  // Licensed under the MIT License.
3
3
  import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
4
  import { z } from "zod";
5
- import { getCurrentUserDetails } from "./auth.js";
5
+ import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
6
  import { getEnumKeys } from "../utils.js";
7
7
  const REPO_TOOLS = {
8
8
  list_repos_by_project: "repo_list_repos_by_project",
@@ -197,12 +197,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
197
197
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
198
198
  skip: z.number().default(0).describe("The number of pull requests to skip."),
199
199
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
200
+ 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."),
200
201
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
201
202
  status: z
202
203
  .enum(getEnumKeys(PullRequestStatus))
203
204
  .default("Active")
204
205
  .describe("Filter pull requests by status. Defaults to 'Active'."),
205
- }, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
206
+ }, async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
206
207
  const connection = await connectionProvider();
207
208
  const gitApi = await connection.getGitApi();
208
209
  // Build the search criteria
@@ -210,7 +211,24 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
210
211
  status: pullRequestStatusStringToInt(status),
211
212
  repositoryId: repositoryId,
212
213
  };
213
- if (created_by_me || i_am_reviewer) {
214
+ if (created_by_user) {
215
+ try {
216
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
217
+ searchCriteria.creatorId = userId;
218
+ }
219
+ catch (error) {
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
225
+ },
226
+ ],
227
+ isError: true,
228
+ };
229
+ }
230
+ }
231
+ else if (created_by_me || i_am_reviewer) {
214
232
  const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
215
233
  const userId = data.authenticatedUser.id;
216
234
  if (created_by_me) {
@@ -247,19 +265,37 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
247
265
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
248
266
  skip: z.number().default(0).describe("The number of pull requests to skip."),
249
267
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
268
+ 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."),
250
269
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
251
270
  status: z
252
271
  .enum(getEnumKeys(PullRequestStatus))
253
272
  .default("Active")
254
273
  .describe("Filter pull requests by status. Defaults to 'Active'."),
255
- }, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
274
+ }, async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
256
275
  const connection = await connectionProvider();
257
276
  const gitApi = await connection.getGitApi();
258
277
  // Build the search criteria
259
278
  const gitPullRequestSearchCriteria = {
260
279
  status: pullRequestStatusStringToInt(status),
261
280
  };
262
- if (created_by_me || i_am_reviewer) {
281
+ if (created_by_user) {
282
+ try {
283
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
284
+ gitPullRequestSearchCriteria.creatorId = userId;
285
+ }
286
+ catch (error) {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ }
298
+ else if (created_by_me || i_am_reviewer) {
263
299
  const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
264
300
  const userId = data.authenticatedUser.id;
265
301
  if (created_by_me) {
@@ -85,22 +85,82 @@ function configureWikiTools(server, tokenProvider, connectionProvider) {
85
85
  };
86
86
  }
87
87
  });
88
- server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content by wikiIdentifier and path.", {
89
- wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
90
- project: z.string().describe("The project name or ID where the wiki is located."),
91
- path: z.string().describe("The path of the wiki page to retrieve content for."),
92
- }, async ({ wikiIdentifier, project, path }) => {
88
+ server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content. Provide either a 'url' parameter OR the combination of 'wikiIdentifier' and 'project' parameters.", {
89
+ url: z
90
+ .string()
91
+ .optional()
92
+ .describe("The full URL of the wiki page to retrieve content for. If provided, wikiIdentifier, project, and path are ignored. Supported patterns: https://dev.azure.com/{org}/{project}/_wiki/wikis/{wikiIdentifier}?pagePath=%2FMy%20Page and https://dev.azure.com/{org}/{project}/_wiki/wikis/{wikiIdentifier}/{pageId}/Page-Title"),
93
+ wikiIdentifier: z.string().optional().describe("The unique identifier of the wiki. Required if url is not provided."),
94
+ project: z.string().optional().describe("The project name or ID where the wiki is located. Required if url is not provided."),
95
+ path: z.string().optional().describe("The path of the wiki page to retrieve content for. Optional, defaults to root page if not provided."),
96
+ }, async ({ url, wikiIdentifier, project, path }) => {
93
97
  try {
98
+ const hasUrl = !!url;
99
+ const hasPair = !!wikiIdentifier && !!project;
100
+ if (hasUrl && hasPair) {
101
+ return { content: [{ type: "text", text: "Error fetching wiki page content: Provide either 'url' OR 'wikiIdentifier' with 'project', not both." }], isError: true };
102
+ }
103
+ if (!hasUrl && !hasPair) {
104
+ return { content: [{ type: "text", text: "Error fetching wiki page content: You must provide either 'url' OR both 'wikiIdentifier' and 'project'." }], isError: true };
105
+ }
94
106
  const connection = await connectionProvider();
95
107
  const wikiApi = await connection.getWikiApi();
96
- const stream = await wikiApi.getPageText(project, wikiIdentifier, path, undefined, undefined, true);
97
- if (!stream) {
98
- return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
108
+ let resolvedProject = project;
109
+ let resolvedWiki = wikiIdentifier;
110
+ let resolvedPath = path;
111
+ let pageContent;
112
+ if (url) {
113
+ const parsed = parseWikiUrl(url);
114
+ if ("error" in parsed) {
115
+ return { content: [{ type: "text", text: `Error fetching wiki page content: ${parsed.error}` }], isError: true };
116
+ }
117
+ resolvedProject = parsed.project;
118
+ resolvedWiki = parsed.wikiIdentifier;
119
+ if (parsed.pagePath) {
120
+ resolvedPath = parsed.pagePath;
121
+ }
122
+ if (parsed.pageId) {
123
+ try {
124
+ let accessToken;
125
+ try {
126
+ accessToken = await tokenProvider();
127
+ }
128
+ catch { }
129
+ const baseUrl = connection.serverUrl.replace(/\/$/, "");
130
+ const restUrl = `${baseUrl}/${resolvedProject}/_apis/wiki/wikis/${resolvedWiki}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
131
+ const resp = await fetch(restUrl, {
132
+ headers: accessToken?.token ? { Authorization: `Bearer ${accessToken.token}` } : {},
133
+ });
134
+ if (resp.ok) {
135
+ const json = await resp.json();
136
+ if (json && typeof json.content === "string") {
137
+ pageContent = json.content;
138
+ }
139
+ else if (json && json.path) {
140
+ resolvedPath = json.path;
141
+ }
142
+ }
143
+ else if (resp.status === 404) {
144
+ return { content: [{ type: "text", text: `Error fetching wiki page content: Page with id ${parsed.pageId} not found` }], isError: true };
145
+ }
146
+ }
147
+ catch { }
148
+ }
99
149
  }
100
- const content = await streamToString(stream);
101
- return {
102
- content: [{ type: "text", text: JSON.stringify(content, null, 2) }],
103
- };
150
+ if (!pageContent) {
151
+ if (!resolvedPath) {
152
+ resolvedPath = "/";
153
+ }
154
+ if (!resolvedProject || !resolvedWiki) {
155
+ return { content: [{ type: "text", text: "Project and wikiIdentifier must be defined to fetch wiki page content." }], isError: true };
156
+ }
157
+ const stream = await wikiApi.getPageText(resolvedProject, resolvedWiki, resolvedPath, undefined, undefined, true);
158
+ if (!stream) {
159
+ return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
160
+ }
161
+ pageContent = await streamToString(stream);
162
+ }
163
+ return { content: [{ type: "text", text: JSON.stringify(pageContent, null, 2) }] };
104
164
  }
105
165
  catch (error) {
106
166
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -224,4 +284,47 @@ function streamToString(stream) {
224
284
  stream.on("error", reject);
225
285
  });
226
286
  }
287
+ // Helper to parse Azure DevOps wiki page URLs.
288
+ // Supported examples:
289
+ // - https://dev.azure.com/org/project/_wiki/wikis/wikiIdentifier?wikiVersion=GBmain&pagePath=%2FHome
290
+ // - https://dev.azure.com/org/project/_wiki/wikis/wikiIdentifier/123/Title-Of-Page
291
+ // Returns either a structured object OR an error message inside { error }.
292
+ function parseWikiUrl(url) {
293
+ try {
294
+ const u = new URL(url);
295
+ // Path segments after host
296
+ // Expect pattern: /{project}/_wiki/wikis/{wikiIdentifier}[/{pageId}/...]
297
+ const segments = u.pathname.split("/").filter(Boolean); // remove empty
298
+ const idx = segments.findIndex((s) => s === "_wiki");
299
+ if (idx < 1 || segments[idx + 1] !== "wikis") {
300
+ return { error: "URL does not match expected wiki pattern (missing /_wiki/wikis/ segment)." };
301
+ }
302
+ const project = segments[idx - 1];
303
+ const wikiIdentifier = segments[idx + 2];
304
+ if (!project || !wikiIdentifier) {
305
+ return { error: "Could not extract project or wikiIdentifier from URL." };
306
+ }
307
+ // Query form with pagePath
308
+ const pagePathParam = u.searchParams.get("pagePath");
309
+ if (pagePathParam) {
310
+ let decoded = decodeURIComponent(pagePathParam);
311
+ if (!decoded.startsWith("/"))
312
+ decoded = "/" + decoded;
313
+ return { project, wikiIdentifier, pagePath: decoded };
314
+ }
315
+ // Path ID form: .../wikis/{wikiIdentifier}/{pageId}/...
316
+ const afterWiki = segments.slice(idx + 3); // elements after wikiIdentifier
317
+ if (afterWiki.length >= 1) {
318
+ const maybeId = parseInt(afterWiki[0], 10);
319
+ if (!isNaN(maybeId)) {
320
+ return { project, wikiIdentifier, pageId: maybeId };
321
+ }
322
+ }
323
+ // If nothing else specified, treat as root page
324
+ return { project, wikiIdentifier, pagePath: "/" };
325
+ }
326
+ catch {
327
+ return { error: "Invalid URL format." };
328
+ }
329
+ }
227
330
  export { WIKI_TOOLS, configureWikiTools };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "1.3.1-nightly.20250821";
1
+ export const packageVersion = "1.3.1-nightly.20250822";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "1.3.1-nightly.20250821",
3
+ "version": "1.3.1-nightly.20250822",
4
4
  "description": "MCP server for interacting with Azure DevOps",
5
5
  "license": "MIT",
6
6
  "author": "Microsoft Corporation",