@azure-devops/mcp 1.3.1-nightly.20250821 → 1.3.1-nightly.20250823
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.
- package/dist/tools/auth.js +42 -1
- package/dist/tools/core.js +2 -22
- package/dist/tools/repos.js +41 -5
- package/dist/tools/wiki.js +115 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools/auth.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/tools/core.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
}
|
package/dist/tools/repos.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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) {
|
package/dist/tools/wiki.js
CHANGED
|
@@ -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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
1
|
+
export const packageVersion = "1.3.1-nightly.20250823";
|