@azure-devops/mcp 0.1.0 → 1.1.0
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/LICENSE.md +21 -21
- package/README.md +314 -247
- package/dist/http.js +52 -0
- package/dist/index.js +18 -15
- package/dist/prompts.js +35 -10
- package/dist/server.js +36 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/builds.js +63 -9
- package/dist/tools/core.js +43 -13
- package/dist/tools/releases.js +13 -7
- package/dist/tools/repos.js +161 -31
- package/dist/tools/search.js +53 -47
- package/dist/tools/testplans.js +12 -18
- package/dist/tools/wiki.js +79 -31
- package/dist/tools/work.js +82 -38
- package/dist/tools/workitems.js +187 -111
- package/dist/tools.js +5 -3
- package/dist/useragent.js +18 -0
- package/dist/utils.js +0 -2
- package/dist/version.js +1 -1
- package/package.json +62 -55
package/dist/tools/repos.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { getCurrentUserDetails } from "./auth.js";
|
|
5
6
|
const REPO_TOOLS = {
|
|
@@ -17,14 +18,38 @@ const REPO_TOOLS = {
|
|
|
17
18
|
update_pull_request_status: "repo_update_pull_request_status",
|
|
18
19
|
reply_to_comment: "repo_reply_to_comment",
|
|
19
20
|
resolve_comment: "repo_resolve_comment",
|
|
21
|
+
search_commits: "repo_search_commits",
|
|
22
|
+
list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
|
|
20
23
|
};
|
|
21
24
|
function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
22
25
|
return branches
|
|
23
26
|
?.flatMap((branch) => (branch.name ? [branch.name] : []))
|
|
24
27
|
?.filter((branch) => branch.startsWith("refs/heads/"))
|
|
25
28
|
.map((branch) => branch.replace("refs/heads/", ""))
|
|
29
|
+
.sort((a, b) => b.localeCompare(a))
|
|
26
30
|
.slice(0, top);
|
|
27
31
|
}
|
|
32
|
+
function pullRequestStatusStringToInt(status) {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case "abandoned":
|
|
35
|
+
return PullRequestStatus.Abandoned.valueOf();
|
|
36
|
+
case "active":
|
|
37
|
+
return PullRequestStatus.Active.valueOf();
|
|
38
|
+
case "all":
|
|
39
|
+
return PullRequestStatus.All.valueOf();
|
|
40
|
+
case "completed":
|
|
41
|
+
return PullRequestStatus.Completed.valueOf();
|
|
42
|
+
case "notSet":
|
|
43
|
+
return PullRequestStatus.NotSet.valueOf();
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unknown pull request status: ${status}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function filterReposByName(repositories, repoNameFilter) {
|
|
49
|
+
const lowerCaseFilter = repoNameFilter.toLowerCase();
|
|
50
|
+
const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
|
|
51
|
+
return filteredByName;
|
|
52
|
+
}
|
|
28
53
|
function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
29
54
|
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
30
55
|
repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
|
|
@@ -33,15 +58,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
33
58
|
title: z.string().describe("The title of the pull request."),
|
|
34
59
|
description: z.string().optional().describe("The description of the pull request. Optional."),
|
|
35
60
|
isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
|
|
36
|
-
|
|
61
|
+
workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
|
|
62
|
+
}, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems }) => {
|
|
37
63
|
const connection = await connectionProvider();
|
|
38
64
|
const gitApi = await connection.getGitApi();
|
|
65
|
+
const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
|
|
39
66
|
const pullRequest = await gitApi.createPullRequest({
|
|
40
67
|
sourceRefName,
|
|
41
68
|
targetRefName,
|
|
42
69
|
title,
|
|
43
70
|
description,
|
|
44
71
|
isDraft,
|
|
72
|
+
workItemRefs: workItemRefs,
|
|
45
73
|
}, repositoryId);
|
|
46
74
|
return {
|
|
47
75
|
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
|
@@ -51,25 +79,28 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
51
79
|
repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
|
|
52
80
|
pullRequestId: z.number().describe("The ID of the pull request to be published."),
|
|
53
81
|
status: z.enum(["active", "abandoned"]).describe("The new status of the pull request. Can be 'active' or 'abandoned'."),
|
|
54
|
-
}, async ({ repositoryId, pullRequestId }) => {
|
|
82
|
+
}, async ({ repositoryId, pullRequestId, status }) => {
|
|
55
83
|
const connection = await connectionProvider();
|
|
56
84
|
const gitApi = await connection.getGitApi();
|
|
57
85
|
const statusValue = status === "active" ? 3 : 2;
|
|
58
86
|
const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
|
|
59
87
|
return {
|
|
60
|
-
content: [
|
|
61
|
-
{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) },
|
|
62
|
-
],
|
|
88
|
+
content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
|
|
63
89
|
};
|
|
64
90
|
});
|
|
65
91
|
server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
|
|
66
92
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
67
|
-
|
|
93
|
+
top: z.number().default(100).describe("The maximum number of repositories to return."),
|
|
94
|
+
skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
|
|
95
|
+
repoNameFilter: z.string().optional().describe("Optional filter to search for repositories by name. If provided, only repositories with names containing this string will be returned."),
|
|
96
|
+
}, async ({ project, top, skip, repoNameFilter }) => {
|
|
68
97
|
const connection = await connectionProvider();
|
|
69
98
|
const gitApi = await connection.getGitApi();
|
|
70
99
|
const repositories = await gitApi.getRepositories(project, false, false, false);
|
|
100
|
+
const filteredRepositories = repoNameFilter ? filterReposByName(repositories, repoNameFilter) : repositories;
|
|
101
|
+
const paginatedRepositories = filteredRepositories?.sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? 0).slice(skip, skip + top);
|
|
71
102
|
// Filter out the irrelevant properties
|
|
72
|
-
const
|
|
103
|
+
const trimmedRepositories = paginatedRepositories?.map((repo) => ({
|
|
73
104
|
id: repo.id,
|
|
74
105
|
name: repo.name,
|
|
75
106
|
isDisabled: repo.isDisabled,
|
|
@@ -79,21 +110,22 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
79
110
|
size: repo.size,
|
|
80
111
|
}));
|
|
81
112
|
return {
|
|
82
|
-
content: [
|
|
83
|
-
{ type: "text", text: JSON.stringify(filteredRepositories, null, 2) },
|
|
84
|
-
],
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
|
|
85
114
|
};
|
|
86
115
|
});
|
|
87
116
|
server.tool(REPO_TOOLS.list_pull_requests_by_repo, "Retrieve a list of pull requests for a given repository.", {
|
|
88
117
|
repositoryId: z.string().describe("The ID of the repository where the pull requests are located."),
|
|
118
|
+
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
119
|
+
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
89
120
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
90
121
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
91
|
-
|
|
122
|
+
status: z.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
|
|
123
|
+
}, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
92
124
|
const connection = await connectionProvider();
|
|
93
125
|
const gitApi = await connection.getGitApi();
|
|
94
126
|
// Build the search criteria
|
|
95
127
|
const searchCriteria = {
|
|
96
|
-
status:
|
|
128
|
+
status: pullRequestStatusStringToInt(status),
|
|
97
129
|
repositoryId: repositoryId,
|
|
98
130
|
};
|
|
99
131
|
if (created_by_me || i_am_reviewer) {
|
|
@@ -106,7 +138,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
106
138
|
searchCriteria.reviewerId = userId;
|
|
107
139
|
}
|
|
108
140
|
}
|
|
109
|
-
const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria
|
|
141
|
+
const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, undefined, // project
|
|
142
|
+
undefined, // maxCommentLength
|
|
143
|
+
skip, top);
|
|
110
144
|
// Filter out the irrelevant properties
|
|
111
145
|
const filteredPullRequests = pullRequests?.map((pr) => ({
|
|
112
146
|
pullRequestId: pr.pullRequestId,
|
|
@@ -121,21 +155,22 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
121
155
|
isDraft: pr.isDraft,
|
|
122
156
|
}));
|
|
123
157
|
return {
|
|
124
|
-
content: [
|
|
125
|
-
{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
|
|
126
|
-
],
|
|
158
|
+
content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
|
|
127
159
|
};
|
|
128
160
|
});
|
|
129
161
|
server.tool(REPO_TOOLS.list_pull_requests_by_project, "Retrieve a list of pull requests for a given project Id or Name.", {
|
|
130
162
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
163
|
+
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
164
|
+
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
131
165
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
132
166
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
133
|
-
|
|
167
|
+
status: z.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
|
|
168
|
+
}, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
134
169
|
const connection = await connectionProvider();
|
|
135
170
|
const gitApi = await connection.getGitApi();
|
|
136
171
|
// Build the search criteria
|
|
137
172
|
const gitPullRequestSearchCriteria = {
|
|
138
|
-
status:
|
|
173
|
+
status: pullRequestStatusStringToInt(status),
|
|
139
174
|
};
|
|
140
175
|
if (created_by_me || i_am_reviewer) {
|
|
141
176
|
const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
|
|
@@ -147,7 +182,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
147
182
|
gitPullRequestSearchCriteria.reviewerId = userId;
|
|
148
183
|
}
|
|
149
184
|
}
|
|
150
|
-
const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria
|
|
185
|
+
const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria, undefined, // maxCommentLength
|
|
186
|
+
skip, top);
|
|
151
187
|
// Filter out the irrelevant properties
|
|
152
188
|
const filteredPullRequests = pullRequests?.map((pr) => ({
|
|
153
189
|
pullRequestId: pr.pullRequestId,
|
|
@@ -163,9 +199,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
163
199
|
isDraft: pr.isDraft,
|
|
164
200
|
}));
|
|
165
201
|
return {
|
|
166
|
-
content: [
|
|
167
|
-
{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
|
|
168
|
-
],
|
|
202
|
+
content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
|
|
169
203
|
};
|
|
170
204
|
});
|
|
171
205
|
server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
|
|
@@ -174,12 +208,15 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
174
208
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
175
209
|
iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
176
210
|
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
177
|
-
|
|
211
|
+
top: z.number().default(100).describe("The maximum number of threads to return."),
|
|
212
|
+
skip: z.number().default(0).describe("The number of threads to skip."),
|
|
213
|
+
}, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip }) => {
|
|
178
214
|
const connection = await connectionProvider();
|
|
179
215
|
const gitApi = await connection.getGitApi();
|
|
180
216
|
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
217
|
+
const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
181
218
|
return {
|
|
182
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
219
|
+
content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
|
|
183
220
|
};
|
|
184
221
|
});
|
|
185
222
|
server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
|
|
@@ -187,13 +224,16 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
187
224
|
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
|
|
188
225
|
threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
|
|
189
226
|
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
190
|
-
|
|
227
|
+
top: z.number().default(100).describe("The maximum number of comments to return."),
|
|
228
|
+
skip: z.number().default(0).describe("The number of comments to skip."),
|
|
229
|
+
}, async ({ repositoryId, pullRequestId, threadId, project, top, skip }) => {
|
|
191
230
|
const connection = await connectionProvider();
|
|
192
231
|
const gitApi = await connection.getGitApi();
|
|
193
232
|
// Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
|
|
194
233
|
const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
|
|
234
|
+
const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
195
235
|
return {
|
|
196
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
236
|
+
content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
|
|
197
237
|
};
|
|
198
238
|
});
|
|
199
239
|
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
@@ -205,19 +245,19 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
205
245
|
const branches = await gitApi.getRefs(repositoryId, undefined);
|
|
206
246
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
207
247
|
return {
|
|
208
|
-
content: [
|
|
209
|
-
{ type: "text", text: JSON.stringify(filteredBranches, null, 2) },
|
|
210
|
-
],
|
|
248
|
+
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
211
249
|
};
|
|
212
250
|
});
|
|
213
251
|
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
|
|
214
252
|
repositoryId: z.string().describe("The ID of the repository where the branches are located."),
|
|
215
|
-
|
|
253
|
+
top: z.number().default(100).describe("The maximum number of branches to return."),
|
|
254
|
+
}, async ({ repositoryId, top }) => {
|
|
216
255
|
const connection = await connectionProvider();
|
|
217
256
|
const gitApi = await connection.getGitApi();
|
|
218
257
|
const branches = await gitApi.getRefs(repositoryId, undefined, undefined, undefined, undefined, true);
|
|
258
|
+
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
219
259
|
return {
|
|
220
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
260
|
+
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
221
261
|
};
|
|
222
262
|
});
|
|
223
263
|
server.tool(REPO_TOOLS.get_repo_by_name_or_id, "Get the repository by project and repository name or ID.", {
|
|
@@ -295,5 +335,95 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
295
335
|
content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
|
|
296
336
|
};
|
|
297
337
|
});
|
|
338
|
+
const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
|
|
339
|
+
server.tool(REPO_TOOLS.search_commits, "Searches for commits in a repository", {
|
|
340
|
+
project: z.string().describe("Project name or ID"),
|
|
341
|
+
repository: z.string().describe("Repository name or ID"),
|
|
342
|
+
fromCommit: z.string().optional().describe("Starting commit ID"),
|
|
343
|
+
toCommit: z.string().optional().describe("Ending commit ID"),
|
|
344
|
+
version: z.string().optional().describe("The name of the branch, tag or commit to filter commits by"),
|
|
345
|
+
versionType: z
|
|
346
|
+
.enum(gitVersionTypeStrings)
|
|
347
|
+
.optional()
|
|
348
|
+
.default(GitVersionType[GitVersionType.Branch])
|
|
349
|
+
.describe("The meaning of the version parameter, e.g., branch, tag or commit"),
|
|
350
|
+
skip: z.number().optional().default(0).describe("Number of commits to skip"),
|
|
351
|
+
top: z.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
352
|
+
includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
|
|
353
|
+
includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
|
|
354
|
+
}, async ({ project, repository, fromCommit, toCommit, version, versionType, skip, top, includeLinks, includeWorkItems }) => {
|
|
355
|
+
try {
|
|
356
|
+
const connection = await connectionProvider();
|
|
357
|
+
const gitApi = await connection.getGitApi();
|
|
358
|
+
const searchCriteria = {
|
|
359
|
+
fromCommitId: fromCommit,
|
|
360
|
+
toCommitId: toCommit,
|
|
361
|
+
includeLinks: includeLinks,
|
|
362
|
+
includeWorkItems: includeWorkItems,
|
|
363
|
+
};
|
|
364
|
+
if (version) {
|
|
365
|
+
const itemVersion = {
|
|
366
|
+
version: version,
|
|
367
|
+
versionType: GitVersionType[versionType],
|
|
368
|
+
};
|
|
369
|
+
searchCriteria.itemVersion = itemVersion;
|
|
370
|
+
}
|
|
371
|
+
const commits = await gitApi.getCommits(repository, searchCriteria, project, skip, // skip
|
|
372
|
+
top);
|
|
373
|
+
return {
|
|
374
|
+
content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
return {
|
|
379
|
+
content: [
|
|
380
|
+
{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: `Error searching commits: ${error instanceof Error ? error.message : String(error)}`,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
isError: true,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
const pullRequestQueryTypesStrings = Object.values(GitPullRequestQueryType).filter((value) => typeof value === "string");
|
|
390
|
+
server.tool(REPO_TOOLS.list_pull_requests_by_commits, "Lists pull requests by commit IDs to find which pull requests contain specific commits", {
|
|
391
|
+
project: z.string().describe("Project name or ID"),
|
|
392
|
+
repository: z.string().describe("Repository name or ID"),
|
|
393
|
+
commits: z.array(z.string()).describe("Array of commit IDs to query for"),
|
|
394
|
+
queryType: z
|
|
395
|
+
.enum(pullRequestQueryTypesStrings)
|
|
396
|
+
.optional()
|
|
397
|
+
.default(GitPullRequestQueryType[GitPullRequestQueryType.LastMergeCommit])
|
|
398
|
+
.describe("Type of query to perform"),
|
|
399
|
+
}, async ({ project, repository, commits, queryType }) => {
|
|
400
|
+
try {
|
|
401
|
+
const connection = await connectionProvider();
|
|
402
|
+
const gitApi = await connection.getGitApi();
|
|
403
|
+
const query = {
|
|
404
|
+
queries: [
|
|
405
|
+
{
|
|
406
|
+
items: commits,
|
|
407
|
+
type: GitPullRequestQueryType[queryType],
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
const queryResult = await gitApi.getPullRequestQuery(query, repository, project);
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: "text", text: JSON.stringify(queryResult, null, 2) }],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `Error querying pull requests by commits: ${error instanceof Error ? error.message : String(error)}`,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
298
428
|
}
|
|
299
429
|
export { REPO_TOOLS, configureRepoTools };
|
package/dist/tools/search.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { apiVersion
|
|
4
|
+
import { apiVersion } from "../utils.js";
|
|
5
5
|
import { orgName } from "../index.js";
|
|
6
6
|
import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
7
7
|
const SEARCH_TOOLS = {
|
|
8
8
|
search_code: "search_code",
|
|
9
9
|
search_wiki: "search_wiki",
|
|
10
|
-
search_workitem: "search_workitem"
|
|
10
|
+
search_workitem: "search_workitem",
|
|
11
11
|
};
|
|
12
|
-
function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
12
|
+
function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
13
13
|
/*
|
|
14
14
|
CODE SEARCH
|
|
15
15
|
Get the code search results for a given search text.
|
|
16
16
|
*/
|
|
17
17
|
server.tool(SEARCH_TOOLS.search_code, "Get the code search results for a given search text.", {
|
|
18
|
-
searchRequest: z
|
|
18
|
+
searchRequest: z
|
|
19
|
+
.object({
|
|
19
20
|
searchText: z.string().describe("Search text to find in code"),
|
|
20
21
|
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
|
|
21
22
|
$top: z.number().default(5).describe("Number of results to return (for pagination)"),
|
|
22
|
-
filters: z
|
|
23
|
+
filters: z
|
|
24
|
+
.object({
|
|
23
25
|
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
24
26
|
Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
|
|
25
27
|
Path: z.array(z.string()).optional().describe("Filter in these paths"),
|
|
@@ -30,9 +32,12 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
30
32
|
// If provided, the search will only return results that match the specified code elements.
|
|
31
33
|
// This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
|
|
32
34
|
// Example: CodeElement: ["MyClass", "MyFunction"]
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
})
|
|
36
|
+
.partial()
|
|
37
|
+
.optional(),
|
|
38
|
+
includeFacets: z.boolean().optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
36
41
|
}, async ({ searchRequest }) => {
|
|
37
42
|
const accessToken = await tokenProvider();
|
|
38
43
|
const connection = await connectionProvider();
|
|
@@ -41,8 +46,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
41
46
|
method: "POST",
|
|
42
47
|
headers: {
|
|
43
48
|
"Content-Type": "application/json",
|
|
44
|
-
Authorization: `Bearer ${accessToken.token}`,
|
|
45
|
-
"User-Agent":
|
|
49
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
50
|
+
"User-Agent": userAgentProvider(),
|
|
46
51
|
},
|
|
47
52
|
body: JSON.stringify(searchRequest),
|
|
48
53
|
});
|
|
@@ -51,32 +56,34 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
51
56
|
}
|
|
52
57
|
const resultText = await response.text();
|
|
53
58
|
const resultJson = JSON.parse(resultText);
|
|
54
|
-
const topResults = Array.isArray(resultJson.results)
|
|
55
|
-
? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length))
|
|
56
|
-
: [];
|
|
59
|
+
const topResults = Array.isArray(resultJson.results) ? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length)) : [];
|
|
57
60
|
const gitApi = await connection.getGitApi();
|
|
58
61
|
const combinedResults = await fetchCombinedResults(topResults, gitApi);
|
|
59
62
|
return {
|
|
60
|
-
content: [
|
|
61
|
-
{ type: "text", text: resultText + JSON.stringify(combinedResults) }
|
|
62
|
-
]
|
|
63
|
+
content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
|
|
63
64
|
};
|
|
64
65
|
});
|
|
65
66
|
/*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
WIKI SEARCH
|
|
68
|
+
Get wiki search results for a given search text.
|
|
69
|
+
*/
|
|
69
70
|
server.tool(SEARCH_TOOLS.search_wiki, "Get wiki search results for a given search text.", {
|
|
70
|
-
searchRequest: z
|
|
71
|
+
searchRequest: z
|
|
72
|
+
.object({
|
|
71
73
|
searchText: z.string().describe("Search text to find in wikis"),
|
|
72
74
|
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
|
|
73
75
|
$top: z.number().default(10).describe("Number of results to return (for pagination)"),
|
|
74
|
-
filters: z
|
|
76
|
+
filters: z
|
|
77
|
+
.object({
|
|
75
78
|
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
76
79
|
Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
})
|
|
81
|
+
.partial()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Filters to apply to the search text"),
|
|
84
|
+
includeFacets: z.boolean().optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict(),
|
|
80
87
|
}, async ({ searchRequest }) => {
|
|
81
88
|
const accessToken = await tokenProvider();
|
|
82
89
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
@@ -84,8 +91,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
84
91
|
method: "POST",
|
|
85
92
|
headers: {
|
|
86
93
|
"Content-Type": "application/json",
|
|
87
|
-
Authorization: `Bearer ${accessToken.token}`,
|
|
88
|
-
"User-Agent":
|
|
94
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
95
|
+
"User-Agent": userAgentProvider(),
|
|
89
96
|
},
|
|
90
97
|
body: JSON.stringify(searchRequest),
|
|
91
98
|
});
|
|
@@ -94,29 +101,32 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
94
101
|
}
|
|
95
102
|
const result = await response.text();
|
|
96
103
|
return {
|
|
97
|
-
content: [
|
|
98
|
-
{ type: "text", text: result }
|
|
99
|
-
]
|
|
104
|
+
content: [{ type: "text", text: result }],
|
|
100
105
|
};
|
|
101
106
|
});
|
|
102
107
|
/*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
WORK ITEM SEARCH
|
|
109
|
+
Get work item search results for a given search text.
|
|
110
|
+
*/
|
|
106
111
|
server.tool(SEARCH_TOOLS.search_workitem, "Get work item search results for a given search text.", {
|
|
107
|
-
searchRequest: z
|
|
112
|
+
searchRequest: z
|
|
113
|
+
.object({
|
|
108
114
|
searchText: z.string().describe("Search text to find in work items"),
|
|
109
115
|
$skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
110
116
|
$top: z.number().default(10).describe("Number of results to return"),
|
|
111
|
-
filters: z
|
|
117
|
+
filters: z
|
|
118
|
+
.object({
|
|
112
119
|
"System.TeamProject": z.array(z.string()).optional().describe("Filter by team project"),
|
|
113
120
|
"System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
|
|
114
121
|
"System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
|
|
115
122
|
"System.State": z.array(z.string()).optional().describe("Filter by state"),
|
|
116
123
|
"System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
})
|
|
125
|
+
.partial()
|
|
126
|
+
.optional(),
|
|
127
|
+
includeFacets: z.boolean().optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict(),
|
|
120
130
|
}, async ({ searchRequest }) => {
|
|
121
131
|
const accessToken = await tokenProvider();
|
|
122
132
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
|
@@ -124,8 +134,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
124
134
|
method: "POST",
|
|
125
135
|
headers: {
|
|
126
136
|
"Content-Type": "application/json",
|
|
127
|
-
Authorization: `Bearer ${accessToken.token}`,
|
|
128
|
-
"User-Agent":
|
|
137
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
138
|
+
"User-Agent": userAgentProvider(),
|
|
129
139
|
},
|
|
130
140
|
body: JSON.stringify(searchRequest),
|
|
131
141
|
});
|
|
@@ -134,9 +144,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
|
134
144
|
}
|
|
135
145
|
const result = await response.text();
|
|
136
146
|
return {
|
|
137
|
-
content: [
|
|
138
|
-
{ type: "text", text: result }
|
|
139
|
-
]
|
|
147
|
+
content: [{ type: "text", text: result }],
|
|
140
148
|
};
|
|
141
149
|
});
|
|
142
150
|
}
|
|
@@ -154,9 +162,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
154
162
|
});
|
|
155
163
|
continue;
|
|
156
164
|
}
|
|
157
|
-
const versionDescriptor = changeId
|
|
158
|
-
? { version: changeId, versionType: 2, versionOptions: 0 }
|
|
159
|
-
: undefined;
|
|
165
|
+
const versionDescriptor = changeId ? { version: changeId, versionType: 2, versionOptions: 0 } : undefined;
|
|
160
166
|
const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
|
|
161
167
|
false, // latestProcessedChange
|
|
162
168
|
false, // download
|
|
@@ -165,12 +171,12 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
165
171
|
true // sanitize
|
|
166
172
|
);
|
|
167
173
|
combinedResults.push({
|
|
168
|
-
gitItem: item
|
|
174
|
+
gitItem: item,
|
|
169
175
|
});
|
|
170
176
|
}
|
|
171
177
|
catch (err) {
|
|
172
178
|
combinedResults.push({
|
|
173
|
-
error: err instanceof Error ? err.message : String(err)
|
|
179
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
180
|
});
|
|
175
181
|
}
|
|
176
182
|
}
|
package/dist/tools/testplans.js
CHANGED
|
@@ -7,7 +7,7 @@ const Test_Plan_Tools = {
|
|
|
7
7
|
add_test_cases_to_suite: "testplan_add_test_cases_to_suite",
|
|
8
8
|
test_results_from_build_id: "testplan_show_test_results_from_build_id",
|
|
9
9
|
list_test_cases: "testplan_list_test_cases",
|
|
10
|
-
list_test_plans: "testplan_list_test_plans"
|
|
10
|
+
list_test_plans: "testplan_list_test_plans",
|
|
11
11
|
};
|
|
12
12
|
function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
13
13
|
/*
|
|
@@ -19,7 +19,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
19
19
|
filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
|
|
20
20
|
includePlanDetails: z.boolean().default(false).describe("Include detailed information about each test plan."),
|
|
21
21
|
continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
|
|
22
|
-
}, async ({ project, filterActivePlans, includePlanDetails, continuationToken
|
|
22
|
+
}, async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
|
|
23
23
|
const owner = ""; //making owner an empty string untill we can figure out how to get owner id
|
|
24
24
|
const connection = await connectionProvider();
|
|
25
25
|
const testPlanApi = await connection.getTestPlanApi();
|
|
@@ -39,7 +39,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
39
39
|
startDate: z.string().optional().describe("The start date of the test plan"),
|
|
40
40
|
endDate: z.string().optional().describe("The end date of the test plan"),
|
|
41
41
|
areaPath: z.string().optional().describe("The area path for the test plan"),
|
|
42
|
-
}, async ({ project, name, iteration, description, startDate, endDate, areaPath
|
|
42
|
+
}, async ({ project, name, iteration, description, startDate, endDate, areaPath }) => {
|
|
43
43
|
const connection = await connectionProvider();
|
|
44
44
|
const testPlanApi = await connection.getTestPlanApi();
|
|
45
45
|
const testPlanToCreate = {
|
|
@@ -52,9 +52,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
52
52
|
};
|
|
53
53
|
const createdTestPlan = await testPlanApi.createTestPlan(testPlanToCreate, project);
|
|
54
54
|
return {
|
|
55
|
-
content: [
|
|
56
|
-
{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) },
|
|
57
|
-
],
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
|
|
58
56
|
};
|
|
59
57
|
});
|
|
60
58
|
/*
|
|
@@ -69,14 +67,10 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
69
67
|
const connection = await connectionProvider();
|
|
70
68
|
const testApi = await connection.getTestApi();
|
|
71
69
|
// If testCaseIds is an array, convert it to comma-separated string
|
|
72
|
-
const testCaseIdsString = Array.isArray(testCaseIds)
|
|
73
|
-
? testCaseIds.join(",")
|
|
74
|
-
: testCaseIds;
|
|
70
|
+
const testCaseIdsString = Array.isArray(testCaseIds) ? testCaseIds.join(",") : testCaseIds;
|
|
75
71
|
const addedTestCases = await testApi.addTestCasesToSuite(project, planId, suiteId, testCaseIdsString);
|
|
76
72
|
return {
|
|
77
|
-
content: [
|
|
78
|
-
{ type: "text", text: JSON.stringify(addedTestCases, null, 2) },
|
|
79
|
-
],
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
|
|
80
74
|
};
|
|
81
75
|
});
|
|
82
76
|
/*
|
|
@@ -169,7 +163,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
169
163
|
}
|
|
170
164
|
/*
|
|
171
165
|
* Helper function to convert steps text to XML format required
|
|
172
|
-
*/
|
|
166
|
+
*/
|
|
173
167
|
function convertStepsToXml(steps) {
|
|
174
168
|
const stepsLines = steps.split("\n").filter((line) => line.trim() !== "");
|
|
175
169
|
let xmlSteps = `<steps id="0" last="${stepsLines.length}">`;
|
|
@@ -178,10 +172,10 @@ function convertStepsToXml(steps) {
|
|
|
178
172
|
if (stepLine) {
|
|
179
173
|
const stepMatch = stepLine.match(/^(\d+)\.\s*(.+)$/);
|
|
180
174
|
const stepText = stepMatch ? stepMatch[2] : stepLine;
|
|
181
|
-
xmlSteps += `
|
|
182
|
-
<step id="${i + 1}" type="ActionStep">
|
|
183
|
-
<parameterizedString isformatted="true">${escapeXml(stepText)}</parameterizedString>
|
|
184
|
-
<parameterizedString isformatted="true">Verify step completes successfully</parameterizedString>
|
|
175
|
+
xmlSteps += `
|
|
176
|
+
<step id="${i + 1}" type="ActionStep">
|
|
177
|
+
<parameterizedString isformatted="true">${escapeXml(stepText)}</parameterizedString>
|
|
178
|
+
<parameterizedString isformatted="true">Verify step completes successfully</parameterizedString>
|
|
185
179
|
</step>`;
|
|
186
180
|
}
|
|
187
181
|
}
|
|
@@ -190,7 +184,7 @@ function convertStepsToXml(steps) {
|
|
|
190
184
|
}
|
|
191
185
|
/*
|
|
192
186
|
* Helper function to escape XML special characters
|
|
193
|
-
*/
|
|
187
|
+
*/
|
|
194
188
|
function escapeXml(unsafe) {
|
|
195
189
|
return unsafe.replace(/[<>&'"]/g, (c) => {
|
|
196
190
|
switch (c) {
|