@azure-devops/mcp 0.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 -0
- package/README.md +247 -0
- package/dist/index.js +51 -0
- package/dist/prompts.js +25 -0
- package/dist/tools/auth.js +20 -0
- package/dist/tools/builds.js +146 -0
- package/dist/tools/core.js +36 -0
- package/dist/tools/releases.js +72 -0
- package/dist/tools/repos.js +299 -0
- package/dist/tools/search.js +179 -0
- package/dist/tools/testplans.js +212 -0
- package/dist/tools/wiki.js +74 -0
- package/dist/tools/work.js +70 -0
- package/dist/tools/workitems.js +485 -0
- package/dist/tools.js +21 -0
- package/dist/utils.js +6 -0
- package/dist/version.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getCurrentUserDetails } from "./auth.js";
|
|
5
|
+
const REPO_TOOLS = {
|
|
6
|
+
list_repos_by_project: "repo_list_repos_by_project",
|
|
7
|
+
list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
|
|
8
|
+
list_pull_requests_by_project: "repo_list_pull_requests_by_project",
|
|
9
|
+
list_branches_by_repo: "repo_list_branches_by_repo",
|
|
10
|
+
list_my_branches_by_repo: "repo_list_my_branches_by_repo",
|
|
11
|
+
list_pull_request_threads: "repo_list_pull_request_threads",
|
|
12
|
+
list_pull_request_thread_comments: "repo_list_pull_request_thread_comments",
|
|
13
|
+
get_repo_by_name_or_id: "repo_get_repo_by_name_or_id",
|
|
14
|
+
get_branch_by_name: "repo_get_branch_by_name",
|
|
15
|
+
get_pull_request_by_id: "repo_get_pull_request_by_id",
|
|
16
|
+
create_pull_request: "repo_create_pull_request",
|
|
17
|
+
update_pull_request_status: "repo_update_pull_request_status",
|
|
18
|
+
reply_to_comment: "repo_reply_to_comment",
|
|
19
|
+
resolve_comment: "repo_resolve_comment",
|
|
20
|
+
};
|
|
21
|
+
function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
22
|
+
return branches
|
|
23
|
+
?.flatMap((branch) => (branch.name ? [branch.name] : []))
|
|
24
|
+
?.filter((branch) => branch.startsWith("refs/heads/"))
|
|
25
|
+
.map((branch) => branch.replace("refs/heads/", ""))
|
|
26
|
+
.slice(0, top);
|
|
27
|
+
}
|
|
28
|
+
function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
29
|
+
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
30
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
|
|
31
|
+
sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
|
|
32
|
+
targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
|
|
33
|
+
title: z.string().describe("The title of the pull request."),
|
|
34
|
+
description: z.string().optional().describe("The description of the pull request. Optional."),
|
|
35
|
+
isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
|
|
36
|
+
}, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, }) => {
|
|
37
|
+
const connection = await connectionProvider();
|
|
38
|
+
const gitApi = await connection.getGitApi();
|
|
39
|
+
const pullRequest = await gitApi.createPullRequest({
|
|
40
|
+
sourceRefName,
|
|
41
|
+
targetRefName,
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
isDraft,
|
|
45
|
+
}, repositoryId);
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
server.tool(REPO_TOOLS.update_pull_request_status, "Update status of an existing pull request to active or abandoned.", {
|
|
51
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
|
|
52
|
+
pullRequestId: z.number().describe("The ID of the pull request to be published."),
|
|
53
|
+
status: z.enum(["active", "abandoned"]).describe("The new status of the pull request. Can be 'active' or 'abandoned'."),
|
|
54
|
+
}, async ({ repositoryId, pullRequestId }) => {
|
|
55
|
+
const connection = await connectionProvider();
|
|
56
|
+
const gitApi = await connection.getGitApi();
|
|
57
|
+
const statusValue = status === "active" ? 3 : 2;
|
|
58
|
+
const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
|
|
66
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
67
|
+
}, async ({ project }) => {
|
|
68
|
+
const connection = await connectionProvider();
|
|
69
|
+
const gitApi = await connection.getGitApi();
|
|
70
|
+
const repositories = await gitApi.getRepositories(project, false, false, false);
|
|
71
|
+
// Filter out the irrelevant properties
|
|
72
|
+
const filteredRepositories = repositories?.map((repo) => ({
|
|
73
|
+
id: repo.id,
|
|
74
|
+
name: repo.name,
|
|
75
|
+
isDisabled: repo.isDisabled,
|
|
76
|
+
isFork: repo.isFork,
|
|
77
|
+
isInMaintenance: repo.isInMaintenance,
|
|
78
|
+
webUrl: repo.webUrl,
|
|
79
|
+
size: repo.size,
|
|
80
|
+
}));
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{ type: "text", text: JSON.stringify(filteredRepositories, null, 2) },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
server.tool(REPO_TOOLS.list_pull_requests_by_repo, "Retrieve a list of pull requests for a given repository.", {
|
|
88
|
+
repositoryId: z.string().describe("The ID of the repository where the pull requests are located."),
|
|
89
|
+
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
90
|
+
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
91
|
+
}, async ({ repositoryId, created_by_me, i_am_reviewer }) => {
|
|
92
|
+
const connection = await connectionProvider();
|
|
93
|
+
const gitApi = await connection.getGitApi();
|
|
94
|
+
// Build the search criteria
|
|
95
|
+
const searchCriteria = {
|
|
96
|
+
status: 1,
|
|
97
|
+
repositoryId: repositoryId,
|
|
98
|
+
};
|
|
99
|
+
if (created_by_me || i_am_reviewer) {
|
|
100
|
+
const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
|
|
101
|
+
const userId = data.authenticatedUser.id;
|
|
102
|
+
if (created_by_me) {
|
|
103
|
+
searchCriteria.creatorId = userId;
|
|
104
|
+
}
|
|
105
|
+
if (i_am_reviewer) {
|
|
106
|
+
searchCriteria.reviewerId = userId;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria);
|
|
110
|
+
// Filter out the irrelevant properties
|
|
111
|
+
const filteredPullRequests = pullRequests?.map((pr) => ({
|
|
112
|
+
pullRequestId: pr.pullRequestId,
|
|
113
|
+
codeReviewId: pr.codeReviewId,
|
|
114
|
+
status: pr.status,
|
|
115
|
+
createdBy: {
|
|
116
|
+
displayName: pr.createdBy?.displayName,
|
|
117
|
+
uniqueName: pr.createdBy?.uniqueName,
|
|
118
|
+
},
|
|
119
|
+
creationDate: pr.creationDate,
|
|
120
|
+
title: pr.title,
|
|
121
|
+
isDraft: pr.isDraft,
|
|
122
|
+
}));
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
server.tool(REPO_TOOLS.list_pull_requests_by_project, "Retrieve a list of pull requests for a given project Id or Name.", {
|
|
130
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
131
|
+
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
132
|
+
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
133
|
+
}, async ({ project, created_by_me, i_am_reviewer }) => {
|
|
134
|
+
const connection = await connectionProvider();
|
|
135
|
+
const gitApi = await connection.getGitApi();
|
|
136
|
+
// Build the search criteria
|
|
137
|
+
const gitPullRequestSearchCriteria = {
|
|
138
|
+
status: 1,
|
|
139
|
+
};
|
|
140
|
+
if (created_by_me || i_am_reviewer) {
|
|
141
|
+
const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
|
|
142
|
+
const userId = data.authenticatedUser.id;
|
|
143
|
+
if (created_by_me) {
|
|
144
|
+
gitPullRequestSearchCriteria.creatorId = userId;
|
|
145
|
+
}
|
|
146
|
+
if (i_am_reviewer) {
|
|
147
|
+
gitPullRequestSearchCriteria.reviewerId = userId;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria);
|
|
151
|
+
// Filter out the irrelevant properties
|
|
152
|
+
const filteredPullRequests = pullRequests?.map((pr) => ({
|
|
153
|
+
pullRequestId: pr.pullRequestId,
|
|
154
|
+
codeReviewId: pr.codeReviewId,
|
|
155
|
+
repository: pr.repository?.name,
|
|
156
|
+
status: pr.status,
|
|
157
|
+
createdBy: {
|
|
158
|
+
displayName: pr.createdBy?.displayName,
|
|
159
|
+
uniqueName: pr.createdBy?.uniqueName,
|
|
160
|
+
},
|
|
161
|
+
creationDate: pr.creationDate,
|
|
162
|
+
title: pr.title,
|
|
163
|
+
isDraft: pr.isDraft,
|
|
164
|
+
}));
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
|
|
172
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
173
|
+
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve threads."),
|
|
174
|
+
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
175
|
+
iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
176
|
+
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
177
|
+
}, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, }) => {
|
|
178
|
+
const connection = await connectionProvider();
|
|
179
|
+
const gitApi = await connection.getGitApi();
|
|
180
|
+
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: JSON.stringify(threads, null, 2) }],
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
|
|
186
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
187
|
+
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
|
|
188
|
+
threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
|
|
189
|
+
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
190
|
+
}, async ({ repositoryId, pullRequestId, threadId, project }) => {
|
|
191
|
+
const connection = await connectionProvider();
|
|
192
|
+
const gitApi = await connection.getGitApi();
|
|
193
|
+
// Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
|
|
194
|
+
const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
|
|
195
|
+
return {
|
|
196
|
+
content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
200
|
+
repositoryId: z.string().describe("The ID of the repository where the branches are located."),
|
|
201
|
+
top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
202
|
+
}, async ({ repositoryId, top }) => {
|
|
203
|
+
const connection = await connectionProvider();
|
|
204
|
+
const gitApi = await connection.getGitApi();
|
|
205
|
+
const branches = await gitApi.getRefs(repositoryId, undefined);
|
|
206
|
+
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{ type: "text", text: JSON.stringify(filteredBranches, null, 2) },
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
|
|
214
|
+
repositoryId: z.string().describe("The ID of the repository where the branches are located."),
|
|
215
|
+
}, async ({ repositoryId }) => {
|
|
216
|
+
const connection = await connectionProvider();
|
|
217
|
+
const gitApi = await connection.getGitApi();
|
|
218
|
+
const branches = await gitApi.getRefs(repositoryId, undefined, undefined, undefined, undefined, true);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
server.tool(REPO_TOOLS.get_repo_by_name_or_id, "Get the repository by project and repository name or ID.", {
|
|
224
|
+
project: z.string().describe("Project name or ID where the repository is located."),
|
|
225
|
+
repositoryNameOrId: z.string().describe("Repository name or ID."),
|
|
226
|
+
}, async ({ project, repositoryNameOrId }) => {
|
|
227
|
+
const connection = await connectionProvider();
|
|
228
|
+
const gitApi = await connection.getGitApi();
|
|
229
|
+
const repositories = await gitApi.getRepositories(project);
|
|
230
|
+
const repository = repositories?.find((repo) => repo.name === repositoryNameOrId || repo.id === repositoryNameOrId);
|
|
231
|
+
if (!repository) {
|
|
232
|
+
throw new Error(`Repository ${repositoryNameOrId} not found in project ${project}`);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
|
|
239
|
+
repositoryId: z.string().describe("The ID of the repository where the branch is located."),
|
|
240
|
+
branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
|
|
241
|
+
}, async ({ repositoryId, branchName }) => {
|
|
242
|
+
const connection = await connectionProvider();
|
|
243
|
+
const gitApi = await connection.getGitApi();
|
|
244
|
+
const branches = await gitApi.getRefs(repositoryId);
|
|
245
|
+
const branch = branches?.find((branch) => branch.name === `refs/heads/${branchName}`);
|
|
246
|
+
if (!branch) {
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: `Branch ${branchName} not found in repository ${repositoryId}`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
|
|
261
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
262
|
+
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
263
|
+
}, async ({ repositoryId, pullRequestId }) => {
|
|
264
|
+
const connection = await connectionProvider();
|
|
265
|
+
const gitApi = await connection.getGitApi();
|
|
266
|
+
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
|
|
272
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
273
|
+
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
274
|
+
threadId: z.number().describe("The ID of the thread to which the comment will be added."),
|
|
275
|
+
content: z.string().describe("The content of the comment to be added."),
|
|
276
|
+
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
277
|
+
}, async ({ repositoryId, pullRequestId, threadId, content, project }) => {
|
|
278
|
+
const connection = await connectionProvider();
|
|
279
|
+
const gitApi = await connection.getGitApi();
|
|
280
|
+
const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
|
|
286
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
287
|
+
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
288
|
+
threadId: z.number().describe("The ID of the thread to be resolved."),
|
|
289
|
+
}, async ({ repositoryId, pullRequestId, threadId }) => {
|
|
290
|
+
const connection = await connectionProvider();
|
|
291
|
+
const gitApi = await connection.getGitApi();
|
|
292
|
+
const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
|
|
293
|
+
repositoryId, pullRequestId, threadId);
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
export { REPO_TOOLS, configureRepoTools };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { apiVersion, userAgent } from "../utils.js";
|
|
5
|
+
import { orgName } from "../index.js";
|
|
6
|
+
import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
7
|
+
const SEARCH_TOOLS = {
|
|
8
|
+
search_code: "search_code",
|
|
9
|
+
search_wiki: "search_wiki",
|
|
10
|
+
search_workitem: "search_workitem"
|
|
11
|
+
};
|
|
12
|
+
function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
13
|
+
/*
|
|
14
|
+
CODE SEARCH
|
|
15
|
+
Get the code search results for a given search text.
|
|
16
|
+
*/
|
|
17
|
+
server.tool(SEARCH_TOOLS.search_code, "Get the code search results for a given search text.", {
|
|
18
|
+
searchRequest: z.object({
|
|
19
|
+
searchText: z.string().describe("Search text to find in code"),
|
|
20
|
+
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
|
|
21
|
+
$top: z.number().default(5).describe("Number of results to return (for pagination)"),
|
|
22
|
+
filters: z.object({
|
|
23
|
+
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
24
|
+
Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
|
|
25
|
+
Path: z.array(z.string()).optional().describe("Filter in these paths"),
|
|
26
|
+
Branch: z.array(z.string()).optional().describe("Filter in these branches"),
|
|
27
|
+
CodeElement: z.array(z.string()).optional().describe("Filter for these code elements (e.g., classes, functions, symbols)"),
|
|
28
|
+
// Note: CodeElement is optional and can be used to filter results by specific code elements.
|
|
29
|
+
// It can be a string or an array of strings.
|
|
30
|
+
// If provided, the search will only return results that match the specified code elements.
|
|
31
|
+
// This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
|
|
32
|
+
// Example: CodeElement: ["MyClass", "MyFunction"]
|
|
33
|
+
}).partial().optional(),
|
|
34
|
+
includeFacets: z.boolean().optional()
|
|
35
|
+
}).strict()
|
|
36
|
+
}, async ({ searchRequest }) => {
|
|
37
|
+
const accessToken = await tokenProvider();
|
|
38
|
+
const connection = await connectionProvider();
|
|
39
|
+
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
45
|
+
"User-Agent": `${userAgent}`
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(searchRequest),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Azure DevOps Code Search API error: ${response.status} ${response.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
const resultText = await response.text();
|
|
53
|
+
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
|
+
: [];
|
|
57
|
+
const gitApi = await connection.getGitApi();
|
|
58
|
+
const combinedResults = await fetchCombinedResults(topResults, gitApi);
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{ type: "text", text: resultText + JSON.stringify(combinedResults) }
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
/*
|
|
66
|
+
WIKI SEARCH
|
|
67
|
+
Get wiki search results for a given search text.
|
|
68
|
+
*/
|
|
69
|
+
server.tool(SEARCH_TOOLS.search_wiki, "Get wiki search results for a given search text.", {
|
|
70
|
+
searchRequest: z.object({
|
|
71
|
+
searchText: z.string().describe("Search text to find in wikis"),
|
|
72
|
+
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
|
|
73
|
+
$top: z.number().default(10).describe("Number of results to return (for pagination)"),
|
|
74
|
+
filters: z.object({
|
|
75
|
+
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
76
|
+
Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
|
|
77
|
+
}).partial().optional().describe("Filters to apply to the search text"),
|
|
78
|
+
includeFacets: z.boolean().optional()
|
|
79
|
+
}).strict()
|
|
80
|
+
}, async ({ searchRequest }) => {
|
|
81
|
+
const accessToken = await tokenProvider();
|
|
82
|
+
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
88
|
+
"User-Agent": `${userAgent}`
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(searchRequest),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
const result = await response.text();
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{ type: "text", text: result }
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
/*
|
|
103
|
+
WORK ITEM SEARCH
|
|
104
|
+
Get work item search results for a given search text.
|
|
105
|
+
*/
|
|
106
|
+
server.tool(SEARCH_TOOLS.search_workitem, "Get work item search results for a given search text.", {
|
|
107
|
+
searchRequest: z.object({
|
|
108
|
+
searchText: z.string().describe("Search text to find in work items"),
|
|
109
|
+
$skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
110
|
+
$top: z.number().default(10).describe("Number of results to return"),
|
|
111
|
+
filters: z.object({
|
|
112
|
+
"System.TeamProject": z.array(z.string()).optional().describe("Filter by team project"),
|
|
113
|
+
"System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
|
|
114
|
+
"System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
|
|
115
|
+
"System.State": z.array(z.string()).optional().describe("Filter by state"),
|
|
116
|
+
"System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
|
|
117
|
+
}).partial().optional(),
|
|
118
|
+
includeFacets: z.boolean().optional()
|
|
119
|
+
}).strict()
|
|
120
|
+
}, async ({ searchRequest }) => {
|
|
121
|
+
const accessToken = await tokenProvider();
|
|
122
|
+
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
|
123
|
+
const response = await fetch(url, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
128
|
+
"User-Agent": `${userAgent}`
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(searchRequest),
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
const result = await response.text();
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{ type: "text", text: result }
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
144
|
+
const combinedResults = [];
|
|
145
|
+
for (const searchResult of topSearchResults) {
|
|
146
|
+
try {
|
|
147
|
+
const projectId = searchResult.project?.id;
|
|
148
|
+
const repositoryId = searchResult.repository?.id;
|
|
149
|
+
const filePath = searchResult.path;
|
|
150
|
+
const changeId = Array.isArray(searchResult.versions) && searchResult.versions.length > 0 ? searchResult.versions[0].changeId : undefined;
|
|
151
|
+
if (!projectId || !repositoryId || !filePath || !changeId) {
|
|
152
|
+
combinedResults.push({
|
|
153
|
+
error: `Missing projectId, repositoryId, filePath, or changeId in the result: ${JSON.stringify(searchResult)}`,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const versionDescriptor = changeId
|
|
158
|
+
? { version: changeId, versionType: 2, versionOptions: 0 }
|
|
159
|
+
: undefined;
|
|
160
|
+
const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
|
|
161
|
+
false, // latestProcessedChange
|
|
162
|
+
false, // download
|
|
163
|
+
versionDescriptor, true, // includeContent
|
|
164
|
+
true, // resolveLfs
|
|
165
|
+
true // sanitize
|
|
166
|
+
);
|
|
167
|
+
combinedResults.push({
|
|
168
|
+
gitItem: item
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
combinedResults.push({
|
|
173
|
+
error: err instanceof Error ? err.message : String(err)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return combinedResults;
|
|
178
|
+
}
|
|
179
|
+
export { SEARCH_TOOLS, configureSearchTools };
|