@azure-devops/mcp 1.2.1 → 1.3.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 +25 -141
- package/dist/shared/tool-validation.js +92 -0
- package/dist/tools/advsec.js +108 -0
- package/dist/tools/core.test.js +1 -0
- package/dist/tools/repos.js +51 -13
- package/dist/tools/testplan.test.js +125 -0
- package/dist/tools/testplans.js +8 -4
- package/dist/tools/utils.js +6 -0
- package/dist/tools/wiki.test.js +87 -0
- package/dist/tools/workitem.test.js +101 -0
- package/dist/tools/workitems.js +85 -3
- package/dist/tools/workitems.test.js +530 -0
- package/dist/tools.js +8 -6
- package/dist/utils.js +26 -0
- package/dist/version.js +1 -1
- package/package.json +4 -2
- package/dist/http.js +0 -52
- package/dist/server.js +0 -36
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { describe, expect, it } from "@jest/globals";
|
|
4
|
+
import { configureWorkItemTools } from "../../../src/tools/workitems";
|
|
5
|
+
import { _mockBacklogs, _mockQuery, _mockQueryResults, _mockWorkItem, _mockWorkItemComment, _mockWorkItemComments, _mockWorkItems, _mockWorkItemsForIteration, _mockWorkItemType } from "../../mocks/work-items";
|
|
6
|
+
describe("configureWorkItemTools", () => {
|
|
7
|
+
let server;
|
|
8
|
+
let tokenProvider;
|
|
9
|
+
let connectionProvider;
|
|
10
|
+
let mockConnection;
|
|
11
|
+
let mockWorkApi;
|
|
12
|
+
let mockWorkItemTrackingApi;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
server = { tool: jest.fn() };
|
|
15
|
+
tokenProvider = jest.fn();
|
|
16
|
+
mockWorkApi = {
|
|
17
|
+
getBacklogs: jest.fn(),
|
|
18
|
+
getBacklogLevelWorkItems: jest.fn(),
|
|
19
|
+
getPredefinedQueryResults: jest.fn(),
|
|
20
|
+
getTeamIterations: jest.fn(),
|
|
21
|
+
getIterationWorkItems: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
mockWorkItemTrackingApi = {
|
|
24
|
+
getWorkItemsBatch: jest.fn(),
|
|
25
|
+
getWorkItem: jest.fn(),
|
|
26
|
+
getComments: jest.fn(),
|
|
27
|
+
addComment: jest.fn(),
|
|
28
|
+
updateWorkItem: jest.fn(),
|
|
29
|
+
createWorkItem: jest.fn(),
|
|
30
|
+
getWorkItemType: jest.fn(),
|
|
31
|
+
getQuery: jest.fn(),
|
|
32
|
+
queryById: jest.fn(),
|
|
33
|
+
};
|
|
34
|
+
mockConnection = {
|
|
35
|
+
getWorkApi: jest.fn().mockResolvedValue(mockWorkApi),
|
|
36
|
+
getWorkItemTrackingApi: jest.fn().mockResolvedValue(mockWorkItemTrackingApi),
|
|
37
|
+
};
|
|
38
|
+
connectionProvider = jest.fn().mockResolvedValue(mockConnection);
|
|
39
|
+
});
|
|
40
|
+
describe("tool registration", () => {
|
|
41
|
+
it("registers core tools on the server", () => {
|
|
42
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
43
|
+
expect(server.tool).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("list_backlogs tool", () => {
|
|
47
|
+
it("should call getBacklogs API with the correct parameters and return the expected result", async () => {
|
|
48
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
49
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_list_backlogs");
|
|
50
|
+
if (!call)
|
|
51
|
+
throw new Error("ado_list_backlogs tool not registered");
|
|
52
|
+
const [, , , handler] = call;
|
|
53
|
+
mockWorkApi.getBacklogs.mockResolvedValue([
|
|
54
|
+
_mockBacklogs,
|
|
55
|
+
]);
|
|
56
|
+
const params = {
|
|
57
|
+
project: "Contoso",
|
|
58
|
+
team: "Fabrikam",
|
|
59
|
+
};
|
|
60
|
+
const result = await handler(params);
|
|
61
|
+
expect(mockWorkApi.getBacklogs).toHaveBeenCalledWith({
|
|
62
|
+
project: params.project,
|
|
63
|
+
team: params.team,
|
|
64
|
+
});
|
|
65
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
66
|
+
_mockBacklogs,
|
|
67
|
+
], null, 2));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("list_backlog_work_items tool", () => {
|
|
71
|
+
it("should call getBacklogLevelWorkItems API with the correct parameters and return the expected result", async () => {
|
|
72
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
73
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_list_backlog_work_items");
|
|
74
|
+
if (!call)
|
|
75
|
+
throw new Error("ado_list_backlog_work_items tool not registered");
|
|
76
|
+
const [, , , handler] = call;
|
|
77
|
+
mockWorkApi.getBacklogLevelWorkItems.mockResolvedValue([
|
|
78
|
+
{
|
|
79
|
+
workItems: [
|
|
80
|
+
{
|
|
81
|
+
rel: null,
|
|
82
|
+
source: null,
|
|
83
|
+
target: {
|
|
84
|
+
id: 50,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
rel: null,
|
|
89
|
+
source: null,
|
|
90
|
+
target: {
|
|
91
|
+
id: 49,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
const params = {
|
|
98
|
+
project: "Contoso",
|
|
99
|
+
team: "Fabrikam",
|
|
100
|
+
backlogId: "Microsoft.FeatureCategory",
|
|
101
|
+
};
|
|
102
|
+
const result = await handler(params);
|
|
103
|
+
expect(mockWorkApi.getBacklogLevelWorkItems).toHaveBeenCalledWith({ project: params.project, team: params.team }, params.backlogId);
|
|
104
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
105
|
+
{
|
|
106
|
+
workItems: [
|
|
107
|
+
{
|
|
108
|
+
rel: null,
|
|
109
|
+
source: null,
|
|
110
|
+
target: {
|
|
111
|
+
id: 50,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
rel: null,
|
|
116
|
+
source: null,
|
|
117
|
+
target: {
|
|
118
|
+
id: 49,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
], null, 2));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("my_work_items tool", () => {
|
|
127
|
+
it("should call getPredefinedQueryResults API with the correct parameters and return the expected result", async () => {
|
|
128
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
129
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_my_work_items");
|
|
130
|
+
if (!call)
|
|
131
|
+
throw new Error("ado_my_work_items tool not registered");
|
|
132
|
+
const [, , , handler] = call;
|
|
133
|
+
mockWorkApi.getPredefinedQueryResults.mockResolvedValue([
|
|
134
|
+
{
|
|
135
|
+
id: "assignedtome",
|
|
136
|
+
name: "Assigned to me",
|
|
137
|
+
url: "https://dev.azure.com/org/project/_apis/work/predefinedQueries/assignedtome",
|
|
138
|
+
webUrl: "https://dev.azure.com/org/project/project/_workitems/assignedtome",
|
|
139
|
+
hasMore: false,
|
|
140
|
+
results: [
|
|
141
|
+
{
|
|
142
|
+
id: 115784,
|
|
143
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115784",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 115794,
|
|
147
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115794",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 115792,
|
|
151
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115792",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
const params = {
|
|
157
|
+
project: "Contoso",
|
|
158
|
+
type: "assignedtome",
|
|
159
|
+
top: 10,
|
|
160
|
+
includeCompleted: false,
|
|
161
|
+
};
|
|
162
|
+
const result = await handler(params);
|
|
163
|
+
expect(mockWorkApi.getPredefinedQueryResults).toHaveBeenCalledWith(params.project, params.type, params.top, params.includeCompleted);
|
|
164
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
165
|
+
{
|
|
166
|
+
id: "assignedtome",
|
|
167
|
+
name: "Assigned to me",
|
|
168
|
+
url: "https://dev.azure.com/org/project/_apis/work/predefinedQueries/assignedtome",
|
|
169
|
+
webUrl: "https://dev.azure.com/org/project/project/_workitems/assignedtome",
|
|
170
|
+
hasMore: false,
|
|
171
|
+
results: [
|
|
172
|
+
{
|
|
173
|
+
id: 115784,
|
|
174
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115784",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 115794,
|
|
178
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115794",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 115792,
|
|
182
|
+
url: "https://dev.azure.com/org/_apis/wit/workItems/115792",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
], null, 2));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe("getWorkItemsBatch tool", () => {
|
|
190
|
+
it("should call workItemApi.getWorkItemsBatch API with the correct parameters and return the expected result", async () => {
|
|
191
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
192
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_work_items_batch_by_ids");
|
|
193
|
+
if (!call)
|
|
194
|
+
throw new Error("ado_get_work_items_batch_by_ids tool not registered");
|
|
195
|
+
const [, , , handler] = call;
|
|
196
|
+
mockWorkItemTrackingApi.getWorkItemsBatch.mockResolvedValue([
|
|
197
|
+
_mockWorkItems,
|
|
198
|
+
]);
|
|
199
|
+
const params = {
|
|
200
|
+
ids: [297, 299, 300],
|
|
201
|
+
project: "Contoso",
|
|
202
|
+
};
|
|
203
|
+
const result = await handler(params);
|
|
204
|
+
expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith({
|
|
205
|
+
ids: params.ids,
|
|
206
|
+
fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags"]
|
|
207
|
+
}, params.project);
|
|
208
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
209
|
+
_mockWorkItems,
|
|
210
|
+
], null, 2));
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe("get_work_item tool", () => {
|
|
214
|
+
it("should call workItemApi.getWorkItem API with the correct parameters and return the expected result", async () => {
|
|
215
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
216
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_work_item");
|
|
217
|
+
if (!call)
|
|
218
|
+
throw new Error("ado_get_work_item tool not registered");
|
|
219
|
+
const [, , , handler] = call;
|
|
220
|
+
mockWorkItemTrackingApi.getWorkItem.mockResolvedValue([
|
|
221
|
+
_mockWorkItem
|
|
222
|
+
]);
|
|
223
|
+
const params = {
|
|
224
|
+
id: 12,
|
|
225
|
+
fields: undefined,
|
|
226
|
+
asOf: undefined,
|
|
227
|
+
expand: "none",
|
|
228
|
+
project: "Contoso",
|
|
229
|
+
};
|
|
230
|
+
const result = await handler(params);
|
|
231
|
+
expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(params.id, params.fields, params.asOf, params.expand, params.project);
|
|
232
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
233
|
+
_mockWorkItem,
|
|
234
|
+
], null, 2));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("list_work_item_comments tool", () => {
|
|
238
|
+
it("should call workItemApi.getComments API with the correct parameters and return the expected result", async () => {
|
|
239
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
240
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_list_work_item_comments");
|
|
241
|
+
if (!call)
|
|
242
|
+
throw new Error("ado_list_work_item_comments tool not registered");
|
|
243
|
+
const [, , , handler] = call;
|
|
244
|
+
mockWorkItemTrackingApi.getComments.mockResolvedValue([
|
|
245
|
+
_mockWorkItemComments
|
|
246
|
+
]);
|
|
247
|
+
const params = {
|
|
248
|
+
project: "Contoso",
|
|
249
|
+
workItemId: 299,
|
|
250
|
+
top: 10,
|
|
251
|
+
};
|
|
252
|
+
const result = await handler(params);
|
|
253
|
+
expect(mockWorkItemTrackingApi.getComments).toHaveBeenCalledWith(params.project, params.workItemId, params.top);
|
|
254
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
255
|
+
_mockWorkItemComments,
|
|
256
|
+
], null, 2));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe("add_work_item_comment tool", () => {
|
|
260
|
+
it("should call workItemApi.addComment API with the correct parameters and return the expected result", async () => {
|
|
261
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
262
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_add_work_item_comment");
|
|
263
|
+
if (!call)
|
|
264
|
+
throw new Error("ado_add_work_item_comment tool not registered");
|
|
265
|
+
const [, , , handler] = call;
|
|
266
|
+
mockWorkItemTrackingApi.addComment.mockResolvedValue([
|
|
267
|
+
_mockWorkItemComment,
|
|
268
|
+
]);
|
|
269
|
+
const params = {
|
|
270
|
+
comment: "hello world!",
|
|
271
|
+
project: "Contoso",
|
|
272
|
+
workItemId: 299,
|
|
273
|
+
};
|
|
274
|
+
const result = await handler(params);
|
|
275
|
+
expect(mockWorkItemTrackingApi.addComment).toHaveBeenCalledWith({ text: params.comment }, params.project, params.workItemId);
|
|
276
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
277
|
+
_mockWorkItemComment
|
|
278
|
+
], null, 2));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe("add_child_work_item tool", () => {
|
|
282
|
+
it("should call workItemApi.add_child_work_item API with the correct parameters and return the expected result", async () => {
|
|
283
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
284
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_add_child_work_item");
|
|
285
|
+
if (!call)
|
|
286
|
+
throw new Error("ado_add_child_work_item tool not registered");
|
|
287
|
+
const [, , , handler] = call;
|
|
288
|
+
mockWorkItemTrackingApi.createWorkItem.mockResolvedValue([
|
|
289
|
+
_mockWorkItem,
|
|
290
|
+
]);
|
|
291
|
+
const params = {
|
|
292
|
+
parentId: 299,
|
|
293
|
+
project: "Contoso",
|
|
294
|
+
workItemType: "Task",
|
|
295
|
+
title: "Sample task",
|
|
296
|
+
description: "This is a sample task",
|
|
297
|
+
areaPath: "Contoso\\Development",
|
|
298
|
+
iterationPath: "Contoso\\Sprint 1",
|
|
299
|
+
};
|
|
300
|
+
const document = [
|
|
301
|
+
{
|
|
302
|
+
op: "add",
|
|
303
|
+
path: "/fields/System.Title",
|
|
304
|
+
value: params.title
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
op: "add",
|
|
308
|
+
path: "/fields/System.Description",
|
|
309
|
+
value: params.description
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
op: "add",
|
|
313
|
+
path: "/relations/-",
|
|
314
|
+
value: {
|
|
315
|
+
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
316
|
+
url: `undefined/${params.project}/_apis/wit/workItems/${params.parentId}`,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
op: "add",
|
|
321
|
+
path: "/fields/System.AreaPath",
|
|
322
|
+
value: params.areaPath,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
op: "add",
|
|
326
|
+
path: "/fields/System.IterationPath",
|
|
327
|
+
value: params.iterationPath,
|
|
328
|
+
}
|
|
329
|
+
];
|
|
330
|
+
const result = await handler(params);
|
|
331
|
+
expect(mockWorkItemTrackingApi.createWorkItem).toHaveBeenCalledWith(null, document, params.project, params.workItemType);
|
|
332
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
333
|
+
_mockWorkItem
|
|
334
|
+
], null, 2));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe("link_work_item_to_pull_request tool", () => {
|
|
338
|
+
it("should call workItemApi.updateWorkItem API with the correct parameters and return the expected result", async () => {
|
|
339
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
340
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_link_work_item_to_pull_request");
|
|
341
|
+
if (!call)
|
|
342
|
+
throw new Error("ado_link_work_item_to_pull_request tool not registered");
|
|
343
|
+
const [, , , handler] = call;
|
|
344
|
+
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue([
|
|
345
|
+
_mockWorkItem,
|
|
346
|
+
]);
|
|
347
|
+
const params = {
|
|
348
|
+
project: "Contoso",
|
|
349
|
+
repositoryId: 12345,
|
|
350
|
+
pullRequestId: 67890,
|
|
351
|
+
workItemId: 131489,
|
|
352
|
+
};
|
|
353
|
+
const artifactPathValue = `${params.project}/${params.repositoryId}/${params.pullRequestId}`;
|
|
354
|
+
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
|
|
355
|
+
const document = [
|
|
356
|
+
{
|
|
357
|
+
op: "add",
|
|
358
|
+
path: "/relations/-",
|
|
359
|
+
value: {
|
|
360
|
+
rel: "ArtifactLink",
|
|
361
|
+
url: vstfsUrl,
|
|
362
|
+
attributes: {
|
|
363
|
+
name: "Pull Request",
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
const result = await handler(params);
|
|
369
|
+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.project);
|
|
370
|
+
expect(result.content[0].text).toBe(JSON.stringify({
|
|
371
|
+
workItemId: 131489,
|
|
372
|
+
pullRequestId: 67890,
|
|
373
|
+
success: true,
|
|
374
|
+
}, null, 2));
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
describe("get_work_items_for_iteration tool", () => {
|
|
378
|
+
it("should call workApi.getIterationWorkItems API with the correct parameters and return the expected result", async () => {
|
|
379
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
380
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_work_items_for_iteration");
|
|
381
|
+
if (!call)
|
|
382
|
+
throw new Error("ado_get_work_items_for_iterationt tool not registered");
|
|
383
|
+
const [, , , handler] = call;
|
|
384
|
+
mockWorkApi.getIterationWorkItems.mockResolvedValue([
|
|
385
|
+
_mockWorkItemsForIteration,
|
|
386
|
+
]);
|
|
387
|
+
const params = {
|
|
388
|
+
project: "Contoso",
|
|
389
|
+
team: "Fabrikam",
|
|
390
|
+
iterationId: "6bfde89e-b22e-422e-814a-e8db432f5a58",
|
|
391
|
+
};
|
|
392
|
+
const result = await handler(params);
|
|
393
|
+
expect(mockWorkApi.getIterationWorkItems).toHaveBeenCalledWith({
|
|
394
|
+
project: params.project,
|
|
395
|
+
team: params.team
|
|
396
|
+
}, params.iterationId);
|
|
397
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
398
|
+
_mockWorkItemsForIteration,
|
|
399
|
+
], null, 2));
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
describe("update_work_item tool", () => {
|
|
403
|
+
it("should call workItemApi.updateWorkItem API with the correct parameters and return the expected result", async () => {
|
|
404
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
405
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_update_work_item");
|
|
406
|
+
if (!call)
|
|
407
|
+
throw new Error("ado_update_work_item tool not registered");
|
|
408
|
+
const [, , , handler] = call;
|
|
409
|
+
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue([
|
|
410
|
+
_mockWorkItem,
|
|
411
|
+
]);
|
|
412
|
+
const params = {
|
|
413
|
+
id: 131489,
|
|
414
|
+
updates: [
|
|
415
|
+
{
|
|
416
|
+
op: "add",
|
|
417
|
+
path: "/fields/System.Title",
|
|
418
|
+
value: "Updated Sample Task"
|
|
419
|
+
}
|
|
420
|
+
]
|
|
421
|
+
};
|
|
422
|
+
const result = await handler(params);
|
|
423
|
+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(null, params.updates, params.id);
|
|
424
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
425
|
+
_mockWorkItem
|
|
426
|
+
], null, 2));
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
describe("get_work_item_type tool", () => {
|
|
430
|
+
it("should call workItemApi.getWorkItemType API with the correct parameters and return the expected result", async () => {
|
|
431
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
432
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_work_item_type");
|
|
433
|
+
if (!call)
|
|
434
|
+
throw new Error("ado_get_work_item_type tool not registered");
|
|
435
|
+
const [, , , handler] = call;
|
|
436
|
+
mockWorkItemTrackingApi.getWorkItemType.mockResolvedValue([
|
|
437
|
+
_mockWorkItemType,
|
|
438
|
+
]);
|
|
439
|
+
const params = {
|
|
440
|
+
project: "Contoso",
|
|
441
|
+
workItemType: "Bug",
|
|
442
|
+
};
|
|
443
|
+
const result = await handler(params);
|
|
444
|
+
expect(mockWorkItemTrackingApi.getWorkItemType).toHaveBeenCalledWith(params.project, params.workItemType);
|
|
445
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
446
|
+
_mockWorkItemType
|
|
447
|
+
], null, 2));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
describe("create_work_item tool", () => {
|
|
451
|
+
it("should call workItemApi.createWorkItem API with the correct parameters and return the expected result", async () => {
|
|
452
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
453
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_create_work_item");
|
|
454
|
+
if (!call)
|
|
455
|
+
throw new Error("ado_create_work_item tool not registered");
|
|
456
|
+
const [, , , handler] = call;
|
|
457
|
+
mockWorkItemTrackingApi.createWorkItem.mockResolvedValue([
|
|
458
|
+
_mockWorkItem,
|
|
459
|
+
]);
|
|
460
|
+
const params = {
|
|
461
|
+
project: "Contoso",
|
|
462
|
+
workItemType: "Task",
|
|
463
|
+
fields: [
|
|
464
|
+
"System.Title", "Hello World!",
|
|
465
|
+
"System.Description", "This is a sample task",
|
|
466
|
+
"System.AreaPath", "Contoso\\Development",
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
const document = Object.entries(params.fields).map(([key, value]) => ({
|
|
470
|
+
op: "add",
|
|
471
|
+
path: `/fields/${key}`,
|
|
472
|
+
value,
|
|
473
|
+
}));
|
|
474
|
+
const result = await handler(params);
|
|
475
|
+
expect(mockWorkItemTrackingApi.createWorkItem).toHaveBeenCalledWith(null, document, params.project, params.workItemType);
|
|
476
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
477
|
+
_mockWorkItem
|
|
478
|
+
], null, 2));
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
describe("get_query tool", () => {
|
|
482
|
+
it("should call workItemApi.getQuery API with the correct parameters and return the expected result", async () => {
|
|
483
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
484
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_query");
|
|
485
|
+
if (!call)
|
|
486
|
+
throw new Error("ado_get_query tool not registered");
|
|
487
|
+
const [, , , handler] = call;
|
|
488
|
+
mockWorkItemTrackingApi.getQuery.mockResolvedValue([
|
|
489
|
+
_mockQuery,
|
|
490
|
+
]);
|
|
491
|
+
const params = {
|
|
492
|
+
project: "Contoso",
|
|
493
|
+
query: "342f0f44-4069-46b1-a940-3d0468979ceb",
|
|
494
|
+
expand: "none",
|
|
495
|
+
depth: 1,
|
|
496
|
+
includeDeleted: false,
|
|
497
|
+
useIsoDateFormat: false,
|
|
498
|
+
};
|
|
499
|
+
const result = await handler(params);
|
|
500
|
+
expect(mockWorkItemTrackingApi.getQuery).toHaveBeenCalledWith(params.project, params.query, params.expand, params.depth, params.includeDeleted, params.useIsoDateFormat);
|
|
501
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
502
|
+
_mockQuery,
|
|
503
|
+
], null, 2));
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
describe("get_query_results_by_id tool", () => {
|
|
507
|
+
it("should call workItemApi.getQueryById API with the correct parameters and return the expected result", async () => {
|
|
508
|
+
configureWorkItemTools(server, tokenProvider, connectionProvider);
|
|
509
|
+
const call = server.tool.mock.calls.find(([toolName]) => toolName === "ado_get_query_results_by_id");
|
|
510
|
+
if (!call)
|
|
511
|
+
throw new Error("ado_get_query_results_by_id tool not registered");
|
|
512
|
+
const [, , , handler] = call;
|
|
513
|
+
mockWorkItemTrackingApi.queryById.mockResolvedValue([
|
|
514
|
+
_mockQueryResults,
|
|
515
|
+
]);
|
|
516
|
+
const params = {
|
|
517
|
+
id: "342f0f44-4069-46b1-a940-3d0468979ceb",
|
|
518
|
+
project: "Contoso",
|
|
519
|
+
team: "Fabrikam",
|
|
520
|
+
timePrecision: false,
|
|
521
|
+
top: 50,
|
|
522
|
+
};
|
|
523
|
+
const result = await handler(params);
|
|
524
|
+
expect(mockWorkItemTrackingApi.queryById).toHaveBeenCalledWith(params.id, { project: params.project, team: params.team }, params.timePrecision, params.top);
|
|
525
|
+
expect(result.content[0].text).toBe(JSON.stringify([
|
|
526
|
+
_mockQueryResults,
|
|
527
|
+
], null, 2));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import {
|
|
4
|
-
import { configureWorkTools } from "./tools/work.js";
|
|
3
|
+
import { configureAdvSecTools } from "./tools/advsec.js";
|
|
5
4
|
import { configureBuildTools } from "./tools/builds.js";
|
|
6
|
-
import {
|
|
7
|
-
import { configureWorkItemTools } from "./tools/workitems.js";
|
|
5
|
+
import { configureCoreTools } from "./tools/core.js";
|
|
8
6
|
import { configureReleaseTools } from "./tools/releases.js";
|
|
9
|
-
import {
|
|
10
|
-
import { configureTestPlanTools } from "./tools/testplans.js";
|
|
7
|
+
import { configureRepoTools } from "./tools/repos.js";
|
|
11
8
|
import { configureSearchTools } from "./tools/search.js";
|
|
9
|
+
import { configureTestPlanTools } from "./tools/testplans.js";
|
|
10
|
+
import { configureWikiTools } from "./tools/wiki.js";
|
|
11
|
+
import { configureWorkTools } from "./tools/work.js";
|
|
12
|
+
import { configureWorkItemTools } from "./tools/workitems.js";
|
|
12
13
|
function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
13
14
|
configureCoreTools(server, tokenProvider, connectionProvider);
|
|
14
15
|
configureWorkTools(server, tokenProvider, connectionProvider);
|
|
@@ -19,5 +20,6 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
|
|
|
19
20
|
configureWikiTools(server, tokenProvider, connectionProvider);
|
|
20
21
|
configureTestPlanTools(server, tokenProvider, connectionProvider);
|
|
21
22
|
configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider);
|
|
23
|
+
configureAdvSecTools(server, tokenProvider, connectionProvider);
|
|
22
24
|
}
|
|
23
25
|
export { configureAllTools };
|
package/dist/utils.js
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
export const apiVersion = "7.2-preview.1";
|
|
4
4
|
export const batchApiVersion = "5.0";
|
|
5
5
|
export const markdownCommentsApiVersion = "7.2-preview.4";
|
|
6
|
+
export function createEnumMapping(enumObject) {
|
|
7
|
+
const mapping = {};
|
|
8
|
+
for (const [key, value] of Object.entries(enumObject)) {
|
|
9
|
+
if (typeof key === "string" && typeof value === "number") {
|
|
10
|
+
mapping[key.toLowerCase()] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return mapping;
|
|
14
|
+
}
|
|
15
|
+
export function mapStringToEnum(value, enumObject, defaultValue) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return defaultValue;
|
|
18
|
+
const enumMapping = createEnumMapping(enumObject);
|
|
19
|
+
return enumMapping[value.toLowerCase()] ?? defaultValue;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Maps an array of strings to an array of enum values, filtering out invalid values.
|
|
23
|
+
* @param values Array of string values to map
|
|
24
|
+
* @param enumObject The enum object to map to
|
|
25
|
+
* @returns Array of valid enum values
|
|
26
|
+
*/
|
|
27
|
+
export function mapStringArrayToEnum(values, enumObject) {
|
|
28
|
+
if (!values)
|
|
29
|
+
return [];
|
|
30
|
+
return values.map((value) => mapStringToEnum(value, enumObject)).filter((v) => v !== undefined);
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
|
|
8
34
|
* This ensures that enum schemas generate string values rather than numeric values.
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.
|
|
1
|
+
export const packageVersion = "1.3.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for interacting with Azure DevOps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"preinstall": "npm config set registry https://registry.npmjs.org/",
|
|
25
25
|
"prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
|
|
26
|
+
"validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
|
|
26
27
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
27
28
|
"prepare": "npm run build",
|
|
28
29
|
"watch": "tsc --watch",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@azure/identity": "^4.10.0",
|
|
40
|
-
"@modelcontextprotocol/sdk": "1.
|
|
41
|
+
"@modelcontextprotocol/sdk": "1.17.0",
|
|
41
42
|
"azure-devops-extension-api": "^4.252.0",
|
|
42
43
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
43
44
|
"azure-devops-node-api": "^15.1.0",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"@types/node": "^22",
|
|
52
53
|
"eslint-config-prettier": "10.1.8",
|
|
53
54
|
"eslint-plugin-header": "^3.1.1",
|
|
55
|
+
"glob": "^11.0.3",
|
|
54
56
|
"jest": "^30.0.2",
|
|
55
57
|
"jest-extended": "^6.0.0",
|
|
56
58
|
"prettier": "3.6.2",
|
package/dist/http.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT License.
|
|
3
|
-
import express from "express";
|
|
4
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
-
import { serverBuildAndConnect } from "./server.js";
|
|
6
|
-
import { packageVersion } from "./version.js";
|
|
7
|
-
const app = express();
|
|
8
|
-
app.use(express.json());
|
|
9
|
-
app.post('/mcp/:orgName', async (req, res) => {
|
|
10
|
-
// In stateless mode, create a new instance of transport and server for each request
|
|
11
|
-
// to ensure complete isolation. A single instance would cause request ID collisions
|
|
12
|
-
// when multiple clients connect concurrently.
|
|
13
|
-
try {
|
|
14
|
-
const transport = new StreamableHTTPServerTransport({
|
|
15
|
-
sessionIdGenerator: undefined,
|
|
16
|
-
});
|
|
17
|
-
const server = await serverBuildAndConnect(req.params.orgName, transport);
|
|
18
|
-
res.on('close', () => {
|
|
19
|
-
transport.close();
|
|
20
|
-
server.close();
|
|
21
|
-
});
|
|
22
|
-
await transport.handleRequest(req, res, req.body);
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
console.error('Error handling MCP request:', error);
|
|
26
|
-
if (!res.headersSent) {
|
|
27
|
-
res.status(500).json({
|
|
28
|
-
jsonrpc: '2.0',
|
|
29
|
-
error: {
|
|
30
|
-
code: -32603,
|
|
31
|
-
message: 'Internal server error',
|
|
32
|
-
},
|
|
33
|
-
id: null,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
app.get('/mcp/:orgName', async (req, res) => {
|
|
39
|
-
console.log('Received GET MCP request');
|
|
40
|
-
res.writeHead(405).end(JSON.stringify({
|
|
41
|
-
jsonrpc: "2.0",
|
|
42
|
-
error: {
|
|
43
|
-
code: -32000,
|
|
44
|
-
message: "Method not allowed."
|
|
45
|
-
},
|
|
46
|
-
id: null
|
|
47
|
-
}));
|
|
48
|
-
});
|
|
49
|
-
const PORT = 3000;
|
|
50
|
-
app.listen(PORT, () => {
|
|
51
|
-
console.log(`Azure DevOps MCP Server with http transport listening on port ${PORT}. Version: ${packageVersion}`);
|
|
52
|
-
});
|