mcp-consultant-tools 0.4.5
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/README.md +968 -0
- package/build/AzureDevOpsService.js +394 -0
- package/build/PowerPlatformService.js +655 -0
- package/build/index.js +2010 -0
- package/package.json +48 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
export class AzureDevOpsService {
|
|
3
|
+
config;
|
|
4
|
+
baseUrl;
|
|
5
|
+
searchUrl;
|
|
6
|
+
authHeader;
|
|
7
|
+
apiVersion;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = {
|
|
10
|
+
...config,
|
|
11
|
+
apiVersion: config.apiVersion || '7.1',
|
|
12
|
+
enableWorkItemWrite: config.enableWorkItemWrite ?? false,
|
|
13
|
+
enableWorkItemDelete: config.enableWorkItemDelete ?? false,
|
|
14
|
+
enableWikiWrite: config.enableWikiWrite ?? false
|
|
15
|
+
};
|
|
16
|
+
this.baseUrl = `https://dev.azure.com/${this.config.organization}`;
|
|
17
|
+
this.searchUrl = `https://almsearch.dev.azure.com/${this.config.organization}`;
|
|
18
|
+
this.apiVersion = this.config.apiVersion;
|
|
19
|
+
// Encode PAT for Basic Auth (format is :PAT encoded in base64)
|
|
20
|
+
this.authHeader = `Basic ${Buffer.from(`:${this.config.pat}`).toString('base64')}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate that a project is in the allowed list
|
|
24
|
+
*/
|
|
25
|
+
validateProject(project) {
|
|
26
|
+
if (!this.config.projects.includes(project)) {
|
|
27
|
+
throw new Error(`Project '${project}' is not in the allowed projects list. Allowed projects: ${this.config.projects.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Make an authenticated request to the Azure DevOps API
|
|
32
|
+
*/
|
|
33
|
+
async makeRequest(endpoint, method = 'GET', data, useSearchUrl = false) {
|
|
34
|
+
try {
|
|
35
|
+
const baseUrl = useSearchUrl ? this.searchUrl : this.baseUrl;
|
|
36
|
+
const url = `${baseUrl}/${endpoint}`;
|
|
37
|
+
const response = await axios({
|
|
38
|
+
method,
|
|
39
|
+
url,
|
|
40
|
+
headers: {
|
|
41
|
+
'Authorization': this.authHeader,
|
|
42
|
+
'Content-Type': method === 'PATCH' ? 'application/json-patch+json' : 'application/json',
|
|
43
|
+
'Accept': 'application/json'
|
|
44
|
+
},
|
|
45
|
+
data
|
|
46
|
+
});
|
|
47
|
+
return response.data;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const errorDetails = error.response?.data?.message || error.response?.data || error.message;
|
|
51
|
+
console.error('Azure DevOps API request failed:', {
|
|
52
|
+
endpoint,
|
|
53
|
+
method,
|
|
54
|
+
status: error.response?.status,
|
|
55
|
+
statusText: error.response?.statusText,
|
|
56
|
+
error: errorDetails
|
|
57
|
+
});
|
|
58
|
+
// Provide user-friendly error messages
|
|
59
|
+
if (error.response?.status === 401) {
|
|
60
|
+
throw new Error('Azure DevOps authentication failed. Please check your PAT token and permissions.');
|
|
61
|
+
}
|
|
62
|
+
if (error.response?.status === 403) {
|
|
63
|
+
throw new Error('Azure DevOps access denied. Please check your PAT scopes and project permissions.');
|
|
64
|
+
}
|
|
65
|
+
if (error.response?.status === 404) {
|
|
66
|
+
throw new Error(`Azure DevOps resource not found: ${endpoint}`);
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Azure DevOps API request failed: ${error.message} - ${JSON.stringify(errorDetails)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ==================== WIKI OPERATIONS ====================
|
|
72
|
+
/**
|
|
73
|
+
* Convert a git path (returned by search) to a wiki path (used by get-page API)
|
|
74
|
+
* Git paths use dashes and .md extensions: /Release-Notes/Page-Name.md
|
|
75
|
+
* Wiki paths use spaces and no extensions: /Release Notes/Page Name
|
|
76
|
+
* @param gitPath The git path from search results
|
|
77
|
+
* @returns The wiki path for use with get-page API
|
|
78
|
+
*/
|
|
79
|
+
convertGitPathToWikiPath(gitPath) {
|
|
80
|
+
return gitPath
|
|
81
|
+
.replace(/\.md$/, '') // Remove .md extension
|
|
82
|
+
.replace(/-/g, ' ') // Replace ALL dashes with spaces
|
|
83
|
+
.replace(/%2D/gi, '-'); // Decode %2D back to - (actual dashes in page names)
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get all wikis in a project
|
|
87
|
+
* @param project The project name
|
|
88
|
+
* @returns List of wikis in the project
|
|
89
|
+
*/
|
|
90
|
+
async getWikis(project) {
|
|
91
|
+
this.validateProject(project);
|
|
92
|
+
const response = await this.makeRequest(`${project}/_apis/wiki/wikis?api-version=${this.apiVersion}`);
|
|
93
|
+
return {
|
|
94
|
+
project,
|
|
95
|
+
totalCount: response.value.length,
|
|
96
|
+
wikis: response.value.map((wiki) => ({
|
|
97
|
+
id: wiki.id,
|
|
98
|
+
name: wiki.name,
|
|
99
|
+
type: wiki.type,
|
|
100
|
+
url: wiki.url,
|
|
101
|
+
projectId: wiki.projectId,
|
|
102
|
+
repositoryId: wiki.repositoryId,
|
|
103
|
+
mappedPath: wiki.mappedPath
|
|
104
|
+
}))
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Search wiki pages across projects
|
|
109
|
+
* @param searchText The text to search for
|
|
110
|
+
* @param project Optional project filter
|
|
111
|
+
* @param maxResults Maximum number of results (default: 25)
|
|
112
|
+
* @returns Search results with highlighted content
|
|
113
|
+
*/
|
|
114
|
+
async searchWikiPages(searchText, project, maxResults = 25) {
|
|
115
|
+
if (project) {
|
|
116
|
+
this.validateProject(project);
|
|
117
|
+
}
|
|
118
|
+
const searchBody = {
|
|
119
|
+
searchText,
|
|
120
|
+
$top: maxResults,
|
|
121
|
+
$skip: 0
|
|
122
|
+
};
|
|
123
|
+
// Add project filter if specified
|
|
124
|
+
if (project) {
|
|
125
|
+
searchBody.filters = {
|
|
126
|
+
Project: [project]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const response = await this.makeRequest(`_apis/search/wikisearchresults?api-version=${this.apiVersion}`, 'POST', searchBody, true // Use search URL
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
searchText,
|
|
133
|
+
project: project || 'all',
|
|
134
|
+
totalCount: response.count || 0,
|
|
135
|
+
results: (response.results || []).map((result) => {
|
|
136
|
+
const gitPath = result.path;
|
|
137
|
+
const wikiPath = this.convertGitPathToWikiPath(gitPath);
|
|
138
|
+
return {
|
|
139
|
+
fileName: result.fileName,
|
|
140
|
+
gitPath: gitPath, // Original git path (for reference)
|
|
141
|
+
path: wikiPath, // Wiki path (for get-page API) - kept as 'path' for backward compatibility
|
|
142
|
+
wikiName: result.wiki?.name,
|
|
143
|
+
wikiId: result.wiki?.id,
|
|
144
|
+
project: result.project?.name,
|
|
145
|
+
highlights: result.hits?.map((hit) => hit.highlights).flat() || []
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get a specific wiki page with content
|
|
152
|
+
* @param project The project name
|
|
153
|
+
* @param wikiId The wiki identifier (ID or name)
|
|
154
|
+
* @param pagePath The path to the page (e.g., "/Setup/Authentication")
|
|
155
|
+
* Accepts both wiki paths (with spaces) and git paths (with dashes and .md)
|
|
156
|
+
* @param includeContent Include page content (default: true)
|
|
157
|
+
* @returns Wiki page with content and metadata
|
|
158
|
+
*/
|
|
159
|
+
async getWikiPage(project, wikiId, pagePath, includeContent = true) {
|
|
160
|
+
this.validateProject(project);
|
|
161
|
+
// Auto-convert git paths to wiki paths for better compatibility
|
|
162
|
+
// If the path ends with .md, it's likely a git path from search results
|
|
163
|
+
let wikiPath = pagePath;
|
|
164
|
+
if (pagePath.endsWith('.md')) {
|
|
165
|
+
wikiPath = this.convertGitPathToWikiPath(pagePath);
|
|
166
|
+
console.log(`Auto-converted git path to wiki path: ${pagePath} -> ${wikiPath}`);
|
|
167
|
+
}
|
|
168
|
+
const response = await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(wikiPath)}&includeContent=${includeContent}&api-version=${this.apiVersion}`);
|
|
169
|
+
// The API returns the page data directly (not wrapped in a 'page' property)
|
|
170
|
+
return {
|
|
171
|
+
id: response.id,
|
|
172
|
+
path: response.path,
|
|
173
|
+
content: response.content,
|
|
174
|
+
gitItemPath: response.gitItemPath,
|
|
175
|
+
subPages: response.subPages || [],
|
|
176
|
+
url: response.url,
|
|
177
|
+
remoteUrl: response.remoteUrl,
|
|
178
|
+
project,
|
|
179
|
+
wikiId
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Create a new wiki page
|
|
184
|
+
* @param project The project name
|
|
185
|
+
* @param wikiId The wiki identifier
|
|
186
|
+
* @param pagePath The path for the new page
|
|
187
|
+
* @param content The markdown content
|
|
188
|
+
* @returns Created page information
|
|
189
|
+
*/
|
|
190
|
+
async createWikiPage(project, wikiId, pagePath, content) {
|
|
191
|
+
this.validateProject(project);
|
|
192
|
+
if (!this.config.enableWikiWrite) {
|
|
193
|
+
throw new Error('Wiki write operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_WRITE=true to enable.');
|
|
194
|
+
}
|
|
195
|
+
const response = await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(pagePath)}&api-version=${this.apiVersion}`, 'PUT', { content });
|
|
196
|
+
return {
|
|
197
|
+
id: response.page?.id,
|
|
198
|
+
path: response.page?.path,
|
|
199
|
+
gitItemPath: response.page?.gitItemPath,
|
|
200
|
+
project,
|
|
201
|
+
wikiId
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Update an existing wiki page
|
|
206
|
+
* @param project The project name
|
|
207
|
+
* @param wikiId The wiki identifier
|
|
208
|
+
* @param pagePath The path to the page
|
|
209
|
+
* @param content The updated markdown content
|
|
210
|
+
* @param version The ETag/version for optimistic concurrency
|
|
211
|
+
* @returns Updated page information
|
|
212
|
+
*/
|
|
213
|
+
async updateWikiPage(project, wikiId, pagePath, content, version) {
|
|
214
|
+
this.validateProject(project);
|
|
215
|
+
if (!this.config.enableWikiWrite) {
|
|
216
|
+
throw new Error('Wiki write operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_WRITE=true to enable.');
|
|
217
|
+
}
|
|
218
|
+
const response = await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(pagePath)}&api-version=${this.apiVersion}`, 'PUT', { content });
|
|
219
|
+
return {
|
|
220
|
+
id: response.page?.id,
|
|
221
|
+
path: response.page?.path,
|
|
222
|
+
gitItemPath: response.page?.gitItemPath,
|
|
223
|
+
project,
|
|
224
|
+
wikiId
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ==================== WORK ITEM OPERATIONS ====================
|
|
228
|
+
/**
|
|
229
|
+
* Get a work item by ID with full details
|
|
230
|
+
* @param project The project name
|
|
231
|
+
* @param workItemId The work item ID
|
|
232
|
+
* @returns Complete work item details
|
|
233
|
+
*/
|
|
234
|
+
async getWorkItem(project, workItemId) {
|
|
235
|
+
this.validateProject(project);
|
|
236
|
+
const response = await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?$expand=all&api-version=${this.apiVersion}`);
|
|
237
|
+
return {
|
|
238
|
+
id: response.id,
|
|
239
|
+
rev: response.rev,
|
|
240
|
+
url: response.url,
|
|
241
|
+
fields: response.fields,
|
|
242
|
+
relations: response.relations || [],
|
|
243
|
+
_links: response._links,
|
|
244
|
+
commentVersionRef: response.commentVersionRef,
|
|
245
|
+
project
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Query work items using WIQL (Work Item Query Language)
|
|
250
|
+
* @param project The project name
|
|
251
|
+
* @param wiql The WIQL query string
|
|
252
|
+
* @param maxResults Maximum number of results (default: 200)
|
|
253
|
+
* @returns Work items matching the query
|
|
254
|
+
*/
|
|
255
|
+
async queryWorkItems(project, wiql, maxResults = 200) {
|
|
256
|
+
this.validateProject(project);
|
|
257
|
+
// Execute WIQL query
|
|
258
|
+
const queryResult = await this.makeRequest(`${project}/_apis/wit/wiql?api-version=${this.apiVersion}`, 'POST', { query: wiql });
|
|
259
|
+
if (!queryResult.workItems || queryResult.workItems.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
query: wiql,
|
|
262
|
+
project,
|
|
263
|
+
totalCount: 0,
|
|
264
|
+
workItems: []
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Get work item IDs (limit to maxResults)
|
|
268
|
+
const workItemIds = queryResult.workItems
|
|
269
|
+
.slice(0, maxResults)
|
|
270
|
+
.map((wi) => wi.id);
|
|
271
|
+
// Batch get full work item details
|
|
272
|
+
const workItems = await this.makeRequest(`${project}/_apis/wit/workitemsbatch?api-version=${this.apiVersion}`, 'POST', {
|
|
273
|
+
ids: workItemIds,
|
|
274
|
+
$expand: 'all'
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
query: wiql,
|
|
278
|
+
project,
|
|
279
|
+
totalCount: workItems.value.length,
|
|
280
|
+
workItems: workItems.value
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get comments/discussion for a work item
|
|
285
|
+
* @param project The project name
|
|
286
|
+
* @param workItemId The work item ID
|
|
287
|
+
* @returns List of comments
|
|
288
|
+
*/
|
|
289
|
+
async getWorkItemComments(project, workItemId) {
|
|
290
|
+
this.validateProject(project);
|
|
291
|
+
const response = await this.makeRequest(`${project}/_apis/wit/workItems/${workItemId}/comments?api-version=${this.apiVersion}`);
|
|
292
|
+
return {
|
|
293
|
+
workItemId,
|
|
294
|
+
project,
|
|
295
|
+
totalCount: response.totalCount || response.value.length,
|
|
296
|
+
comments: response.value.map((comment) => ({
|
|
297
|
+
id: comment.id,
|
|
298
|
+
text: comment.text,
|
|
299
|
+
createdBy: comment.createdBy?.displayName,
|
|
300
|
+
createdDate: comment.createdDate,
|
|
301
|
+
modifiedBy: comment.modifiedBy?.displayName,
|
|
302
|
+
modifiedDate: comment.modifiedDate,
|
|
303
|
+
url: comment.url
|
|
304
|
+
}))
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Add a comment to a work item
|
|
309
|
+
* @param project The project name
|
|
310
|
+
* @param workItemId The work item ID
|
|
311
|
+
* @param commentText The comment text (supports markdown)
|
|
312
|
+
* @returns Created comment information
|
|
313
|
+
*/
|
|
314
|
+
async addWorkItemComment(project, workItemId, commentText) {
|
|
315
|
+
this.validateProject(project);
|
|
316
|
+
if (!this.config.enableWorkItemWrite) {
|
|
317
|
+
throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
|
|
318
|
+
}
|
|
319
|
+
const response = await this.makeRequest(`${project}/_apis/wit/workItems/${workItemId}/comments?api-version=${this.apiVersion}`, 'POST', { text: commentText });
|
|
320
|
+
return {
|
|
321
|
+
id: response.id,
|
|
322
|
+
workItemId,
|
|
323
|
+
project,
|
|
324
|
+
text: response.text,
|
|
325
|
+
createdBy: response.createdBy?.displayName,
|
|
326
|
+
createdDate: response.createdDate
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Update a work item using JSON Patch operations
|
|
331
|
+
* @param project The project name
|
|
332
|
+
* @param workItemId The work item ID
|
|
333
|
+
* @param patchOperations Array of JSON Patch operations
|
|
334
|
+
* @returns Updated work item
|
|
335
|
+
*/
|
|
336
|
+
async updateWorkItem(project, workItemId, patchOperations) {
|
|
337
|
+
this.validateProject(project);
|
|
338
|
+
if (!this.config.enableWorkItemWrite) {
|
|
339
|
+
throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
|
|
340
|
+
}
|
|
341
|
+
const response = await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?api-version=${this.apiVersion}`, 'PATCH', patchOperations);
|
|
342
|
+
return {
|
|
343
|
+
id: response.id,
|
|
344
|
+
rev: response.rev,
|
|
345
|
+
fields: response.fields,
|
|
346
|
+
project
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Create a new work item
|
|
351
|
+
* @param project The project name
|
|
352
|
+
* @param workItemType The work item type (e.g., "Bug", "Task", "User Story")
|
|
353
|
+
* @param fields Object with field values (e.g., { "System.Title": "Bug title" })
|
|
354
|
+
* @returns Created work item
|
|
355
|
+
*/
|
|
356
|
+
async createWorkItem(project, workItemType, fields) {
|
|
357
|
+
this.validateProject(project);
|
|
358
|
+
if (!this.config.enableWorkItemWrite) {
|
|
359
|
+
throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
|
|
360
|
+
}
|
|
361
|
+
// Convert fields object to JSON Patch operations
|
|
362
|
+
const patchOperations = Object.keys(fields).map(field => ({
|
|
363
|
+
op: 'add',
|
|
364
|
+
path: `/fields/${field}`,
|
|
365
|
+
value: fields[field]
|
|
366
|
+
}));
|
|
367
|
+
const response = await this.makeRequest(`${project}/_apis/wit/workitems/$${workItemType}?api-version=${this.apiVersion}`, 'PATCH', patchOperations);
|
|
368
|
+
return {
|
|
369
|
+
id: response.id,
|
|
370
|
+
rev: response.rev,
|
|
371
|
+
fields: response.fields,
|
|
372
|
+
url: response._links?.html?.href,
|
|
373
|
+
project
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Delete a work item
|
|
378
|
+
* @param project The project name
|
|
379
|
+
* @param workItemId The work item ID
|
|
380
|
+
* @returns Deletion confirmation
|
|
381
|
+
*/
|
|
382
|
+
async deleteWorkItem(project, workItemId) {
|
|
383
|
+
this.validateProject(project);
|
|
384
|
+
if (!this.config.enableWorkItemDelete) {
|
|
385
|
+
throw new Error('Work item delete operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_DELETE=true to enable.');
|
|
386
|
+
}
|
|
387
|
+
await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?api-version=${this.apiVersion}`, 'DELETE');
|
|
388
|
+
return {
|
|
389
|
+
workItemId,
|
|
390
|
+
project,
|
|
391
|
+
deleted: true
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|