@radholm/azure-devops-mcp 1.0.0-beta.1

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.
@@ -0,0 +1,196 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { z } from "zod";
4
+ import { apiVersion } from "../utils.js";
5
+ import { orgName, isOnPrem, orgUrl } 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, userAgentProvider) {
13
+ server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
14
+ searchText: z.string().describe("Keywords to search for in code repositories"),
15
+ project: z
16
+ .union([z.string().transform((value) => [value]), z.array(z.string())])
17
+ .optional()
18
+ .describe("Filter by projects"),
19
+ repository: z.array(z.string()).optional().describe("Filter by repositories"),
20
+ path: z.array(z.string()).optional().describe("Filter by paths"),
21
+ branch: z.array(z.string()).optional().describe("Filter by branches"),
22
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
23
+ skip: z.coerce.number().default(0).describe("Number of results to skip"),
24
+ top: z.coerce.number().default(5).describe("Maximum number of results to return"),
25
+ }, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
26
+ const accessToken = await tokenProvider();
27
+ const connection = await connectionProvider();
28
+ const searchBaseUrl = isOnPrem ? `${orgUrl}` : `https://almsearch.dev.azure.com/${orgName}`;
29
+ const url = `${searchBaseUrl}/_apis/search/codesearchresults?api-version=${apiVersion}`;
30
+ const requestBody = {
31
+ searchText,
32
+ includeFacets,
33
+ $skip: skip,
34
+ $top: top,
35
+ };
36
+ const filters = {};
37
+ if (project && project.length > 0)
38
+ filters.Project = project;
39
+ if (repository && repository.length > 0)
40
+ filters.Repository = repository;
41
+ if (path && path.length > 0)
42
+ filters.Path = path;
43
+ if (branch && branch.length > 0)
44
+ filters.Branch = branch;
45
+ if (Object.keys(filters).length > 0) {
46
+ requestBody.filters = filters;
47
+ }
48
+ const response = await fetch(url, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ "Authorization": `Bearer ${accessToken}`,
53
+ "User-Agent": userAgentProvider(),
54
+ },
55
+ body: JSON.stringify(requestBody),
56
+ });
57
+ if (!response.ok) {
58
+ throw new Error(`Azure DevOps Code Search API error: ${response.status} ${response.statusText}`);
59
+ }
60
+ const resultText = await response.text();
61
+ const resultJson = JSON.parse(resultText);
62
+ const gitApi = await connection.getGitApi();
63
+ const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
64
+ return {
65
+ content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
66
+ };
67
+ });
68
+ server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
69
+ searchText: z.string().describe("Keywords to search for wiki pages"),
70
+ project: z.array(z.string()).optional().describe("Filter by projects"),
71
+ wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
72
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
73
+ skip: z.coerce.number().default(0).describe("Number of results to skip"),
74
+ top: z.coerce.number().default(10).describe("Maximum number of results to return"),
75
+ }, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
76
+ const accessToken = await tokenProvider();
77
+ const searchBaseUrl = isOnPrem ? `${orgUrl}` : `https://almsearch.dev.azure.com/${orgName}`;
78
+ const url = `${searchBaseUrl}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
79
+ const requestBody = {
80
+ searchText,
81
+ includeFacets,
82
+ $skip: skip,
83
+ $top: top,
84
+ };
85
+ const filters = {};
86
+ if (project && project.length > 0)
87
+ filters.Project = project;
88
+ if (wiki && wiki.length > 0)
89
+ filters.Wiki = wiki;
90
+ if (Object.keys(filters).length > 0) {
91
+ requestBody.filters = filters;
92
+ }
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ "Authorization": `Bearer ${accessToken}`,
98
+ "User-Agent": userAgentProvider(),
99
+ },
100
+ body: JSON.stringify(requestBody),
101
+ });
102
+ if (!response.ok) {
103
+ throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
104
+ }
105
+ const result = await response.text();
106
+ return {
107
+ content: [{ type: "text", text: result }],
108
+ };
109
+ });
110
+ server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
111
+ searchText: z.string().describe("Search text to find in work items"),
112
+ project: z.array(z.string()).optional().describe("Filter by projects"),
113
+ areaPath: z.array(z.string()).optional().describe("Filter by area paths"),
114
+ workItemType: z.array(z.string()).optional().describe("Filter by work item types"),
115
+ state: z.array(z.string()).optional().describe("Filter by work item states"),
116
+ assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
117
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
118
+ skip: z.coerce.number().default(0).describe("Number of results to skip for pagination"),
119
+ top: z.coerce.number().default(10).describe("Number of results to return"),
120
+ }, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
121
+ const accessToken = await tokenProvider();
122
+ const searchBaseUrl = isOnPrem ? `${orgUrl}` : `https://almsearch.dev.azure.com/${orgName}`;
123
+ const url = `${searchBaseUrl}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
124
+ const requestBody = {
125
+ searchText,
126
+ includeFacets,
127
+ $skip: skip,
128
+ $top: top,
129
+ };
130
+ const filters = {};
131
+ if (project && project.length > 0)
132
+ filters["System.TeamProject"] = project;
133
+ if (areaPath && areaPath.length > 0)
134
+ filters["System.AreaPath"] = areaPath;
135
+ if (workItemType && workItemType.length > 0)
136
+ filters["System.WorkItemType"] = workItemType;
137
+ if (state && state.length > 0)
138
+ filters["System.State"] = state;
139
+ if (assignedTo && assignedTo.length > 0)
140
+ filters["System.AssignedTo"] = assignedTo;
141
+ if (Object.keys(filters).length > 0) {
142
+ requestBody.filters = filters;
143
+ }
144
+ const response = await fetch(url, {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ "Authorization": `Bearer ${accessToken}`,
149
+ "User-Agent": userAgentProvider(),
150
+ },
151
+ body: JSON.stringify(requestBody),
152
+ });
153
+ if (!response.ok) {
154
+ throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
155
+ }
156
+ const result = await response.text();
157
+ return {
158
+ content: [{ type: "text", text: result }],
159
+ };
160
+ });
161
+ }
162
+ async function fetchCombinedResults(topSearchResults, gitApi) {
163
+ const combinedResults = [];
164
+ for (const searchResult of topSearchResults) {
165
+ try {
166
+ const projectId = searchResult.project?.id;
167
+ const repositoryId = searchResult.repository?.id;
168
+ const filePath = searchResult.path;
169
+ const changeId = Array.isArray(searchResult.versions) && searchResult.versions.length > 0 ? searchResult.versions[0].changeId : undefined;
170
+ if (!projectId || !repositoryId || !filePath || !changeId) {
171
+ combinedResults.push({
172
+ error: `Missing projectId, repositoryId, filePath, or changeId in the result: ${JSON.stringify(searchResult)}`,
173
+ });
174
+ continue;
175
+ }
176
+ const versionDescriptor = changeId ? { version: changeId, versionType: 2, versionOptions: 0 } : undefined;
177
+ const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
178
+ false, // latestProcessedChange
179
+ false, // download
180
+ versionDescriptor, true, // includeContent
181
+ true, // resolveLfs
182
+ true // sanitize
183
+ );
184
+ combinedResults.push({
185
+ gitItem: item,
186
+ });
187
+ }
188
+ catch (err) {
189
+ combinedResults.push({
190
+ error: err instanceof Error ? err.message : String(err),
191
+ });
192
+ }
193
+ }
194
+ return combinedResults;
195
+ }
196
+ export { SEARCH_TOOLS, configureSearchTools };