@azure-devops/mcp 2.5.0 → 2.6.0-nightly.20260419

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.
@@ -12,13 +12,16 @@ const SEARCH_TOOLS = {
12
12
  function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
13
13
  server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
14
14
  searchText: z.string().describe("Keywords to search for in code repositories"),
15
- project: z.array(z.string()).optional().describe("Filter by projects"),
15
+ project: z
16
+ .union([z.string().transform((value) => [value]), z.array(z.string())])
17
+ .optional()
18
+ .describe("Filter by projects"),
16
19
  repository: z.array(z.string()).optional().describe("Filter by repositories"),
17
20
  path: z.array(z.string()).optional().describe("Filter by paths"),
18
21
  branch: z.array(z.string()).optional().describe("Filter by branches"),
19
22
  includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
20
- skip: z.number().default(0).describe("Number of results to skip"),
21
- top: z.number().default(5).describe("Maximum number of results to return"),
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"),
22
25
  }, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
23
26
  const accessToken = await tokenProvider();
24
27
  const connection = await connectionProvider();
@@ -66,8 +69,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
66
69
  project: z.array(z.string()).optional().describe("Filter by projects"),
67
70
  wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
68
71
  includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
69
- skip: z.number().default(0).describe("Number of results to skip"),
70
- top: z.number().default(10).describe("Maximum number of results to return"),
72
+ skip: z.coerce.number().default(0).describe("Number of results to skip"),
73
+ top: z.coerce.number().default(10).describe("Maximum number of results to return"),
71
74
  }, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
72
75
  const accessToken = await tokenProvider();
73
76
  const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
@@ -110,8 +113,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
110
113
  state: z.array(z.string()).optional().describe("Filter by work item states"),
111
114
  assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
112
115
  includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
113
- skip: z.number().default(0).describe("Number of results to skip for pagination"),
114
- top: z.number().default(10).describe("Number of results to return"),
116
+ skip: z.coerce.number().default(0).describe("Number of results to skip for pagination"),
117
+ top: z.coerce.number().default(10).describe("Number of results to return"),
115
118
  }, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
116
119
  const accessToken = await tokenProvider();
117
120
  const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { SuiteExpand } from "azure-devops-node-api/interfaces/TestPlanInterfaces.js";
4
3
  import { z } from "zod";
4
+ import { apiVersion } from "../utils.js";
5
5
  const Test_Plan_Tools = {
6
6
  create_test_plan: "testplan_create_test_plan",
7
7
  create_test_case: "testplan_create_test_case",
@@ -13,7 +13,7 @@ const Test_Plan_Tools = {
13
13
  list_test_suites: "testplan_list_test_suites",
14
14
  create_test_suite: "testplan_create_test_suite",
15
15
  };
16
- function configureTestPlanTools(server, _, connectionProvider) {
16
+ function configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider) {
17
17
  server.tool(Test_Plan_Tools.list_test_plans, "Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.", {
18
18
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
19
19
  filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
@@ -21,12 +21,42 @@ function configureTestPlanTools(server, _, connectionProvider) {
21
21
  continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
22
22
  }, async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
23
23
  try {
24
- const owner = ""; //making owner an empty string untill we can figure out how to get owner id
25
24
  const connection = await connectionProvider();
26
- const testPlanApi = await connection.getTestPlanApi();
27
- const testPlans = await testPlanApi.getTestPlans(project, owner, continuationToken, includePlanDetails, filterActivePlans);
25
+ const accessToken = await tokenProvider();
26
+ const params = new URLSearchParams({ "api-version": apiVersion });
27
+ if (filterActivePlans)
28
+ params.append("filterActivePlans", "true");
29
+ if (includePlanDetails)
30
+ params.append("includePlanDetails", "true");
31
+ if (continuationToken)
32
+ params.append("continuationToken", continuationToken);
33
+ const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans?${params.toString()}`;
34
+ const headers = {
35
+ Authorization: `Bearer ${accessToken}`,
36
+ };
37
+ const userAgent = userAgentProvider?.();
38
+ if (userAgent) {
39
+ headers["User-Agent"] = userAgent;
40
+ }
41
+ const response = await fetch(url, {
42
+ method: "GET",
43
+ headers,
44
+ });
45
+ if (!response.ok) {
46
+ const errorText = await response.text();
47
+ throw new Error(`Failed to list test plans (${response.status}): ${errorText}`);
48
+ }
49
+ const body = await response.json();
50
+ const testPlans = body.value ?? [];
51
+ const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
52
+ const result = {
53
+ testPlans: testPlans,
54
+ };
55
+ if (nextToken) {
56
+ result.continuationToken = nextToken;
57
+ }
28
58
  return {
29
- content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
59
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
30
60
  };
31
61
  }
32
62
  catch (error) {
@@ -72,8 +102,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
72
102
  });
73
103
  server.tool(Test_Plan_Tools.create_test_suite, "Creates a new test suite in a test plan.", {
74
104
  project: z.string().describe("Project ID or project name"),
75
- planId: z.number().describe("ID of the test plan that contains the suites"),
76
- parentSuiteId: z.number().describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
105
+ planId: z.coerce.number().min(1).describe("ID of the test plan that contains the suites"),
106
+ parentSuiteId: z.coerce.number().min(1).describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
77
107
  name: z.string().describe("Name of the child test suite"),
78
108
  }, async ({ project, planId, parentSuiteId, name }) => {
79
109
  const maxRetries = 5;
@@ -120,8 +150,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
120
150
  });
121
151
  server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
122
152
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
123
- planId: z.number().describe("The ID of the test plan."),
124
- suiteId: z.number().describe("The ID of the test suite."),
153
+ planId: z.coerce.number().min(1).describe("The ID of the test plan."),
154
+ suiteId: z.coerce.number().min(1).describe("The ID of the test suite."),
125
155
  testCaseIds: z.string().or(z.array(z.string())).describe("The ID(s) of the test case(s) to add. "),
126
156
  }, async ({ project, planId, suiteId, testCaseIds }) => {
127
157
  try {
@@ -149,10 +179,10 @@ function configureTestPlanTools(server, _, connectionProvider) {
149
179
  .string()
150
180
  .optional()
151
181
  .describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
152
- priority: z.number().optional().describe("The priority of the test case."),
182
+ priority: z.coerce.number().optional().describe("The priority of the test case."),
153
183
  areaPath: z.string().optional().describe("The area path for the test case."),
154
184
  iterationPath: z.string().optional().describe("The iteration path for the test case."),
155
- testsWorkItemId: z.number().optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
185
+ testsWorkItemId: z.coerce.number().min(1).optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
156
186
  }, async ({ project, title, steps, priority, areaPath, iterationPath, testsWorkItemId }) => {
157
187
  try {
158
188
  const connection = await connectionProvider();
@@ -220,7 +250,7 @@ function configureTestPlanTools(server, _, connectionProvider) {
220
250
  }
221
251
  });
222
252
  server.tool(Test_Plan_Tools.update_test_case_steps, "Update an existing test case work item.", {
223
- id: z.number().describe("The ID of the test case work item to update."),
253
+ id: z.coerce.number().min(1).describe("The ID of the test case work item to update."),
224
254
  steps: z
225
255
  .string()
226
256
  .describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
@@ -256,15 +286,43 @@ function configureTestPlanTools(server, _, connectionProvider) {
256
286
  });
257
287
  server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
258
288
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
259
- planid: z.number().describe("The ID of the test plan."),
260
- suiteid: z.number().describe("The ID of the test suite."),
261
- }, async ({ project, planid, suiteid }) => {
289
+ planid: z.coerce.number().min(1).describe("The ID of the test plan."),
290
+ suiteid: z.coerce.number().min(1).describe("The ID of the test suite."),
291
+ continuationToken: z.string().optional().describe("Token to continue fetching test cases from a previous request."),
292
+ }, async ({ project, planid, suiteid, continuationToken }) => {
262
293
  try {
263
294
  const connection = await connectionProvider();
264
- const coreApi = await connection.getTestPlanApi();
265
- const testcases = await coreApi.getTestCaseList(project, planid, suiteid);
295
+ const accessToken = await tokenProvider();
296
+ const params = new URLSearchParams({ "api-version": "7.2-preview.3" });
297
+ if (continuationToken)
298
+ params.append("continuationToken", continuationToken);
299
+ const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans/${planid}/Suites/${suiteid}/TestCase?${params.toString()}`;
300
+ const headers = {
301
+ Authorization: `Bearer ${accessToken}`,
302
+ };
303
+ const userAgent = userAgentProvider?.();
304
+ if (userAgent) {
305
+ headers["User-Agent"] = userAgent;
306
+ }
307
+ const response = await fetch(url, {
308
+ method: "GET",
309
+ headers,
310
+ });
311
+ if (!response.ok) {
312
+ const errorText = await response.text();
313
+ throw new Error(`Failed to list test cases (${response.status}): ${errorText}`);
314
+ }
315
+ const body = await response.json();
316
+ const testcases = body.value ?? [];
317
+ const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
318
+ const result = {
319
+ testCases: testcases,
320
+ };
321
+ if (nextToken) {
322
+ result.continuationToken = nextToken;
323
+ }
266
324
  return {
267
- content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
325
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
268
326
  };
269
327
  }
270
328
  catch (error) {
@@ -277,7 +335,7 @@ function configureTestPlanTools(server, _, connectionProvider) {
277
335
  });
278
336
  server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID. Can filter by test outcome (e.g. Failed, Passed, Aborted). Returns test case titles, error messages, stack traces, and outcomes. Efficiently handles builds with large numbers of test runs.", {
279
337
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
280
- buildid: z.number().describe("The ID of the build."),
338
+ buildid: z.coerce.number().min(1).describe("The ID of the build."),
281
339
  outcomes: z.array(z.string()).optional().describe("Filter results by test outcome, e.g. ['Failed', 'Passed', 'Aborted']."),
282
340
  }, async ({ project, buildid, outcomes }) => {
283
341
  try {
@@ -329,14 +387,34 @@ function configureTestPlanTools(server, _, connectionProvider) {
329
387
  });
330
388
  server.tool(Test_Plan_Tools.list_test_suites, "Retrieve a paginated list of test suites from an Azure DevOps project and Test Plan Id.", {
331
389
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
332
- planId: z.number().describe("The ID of the test plan."),
390
+ planId: z.coerce.number().min(1).describe("The ID of the test plan."),
333
391
  continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
334
392
  }, async ({ project, planId, continuationToken }) => {
335
393
  try {
336
394
  const connection = await connectionProvider();
337
- const testPlanApi = await connection.getTestPlanApi();
338
- const expand = SuiteExpand.Children;
339
- const testSuites = await testPlanApi.getTestSuitesForPlan(project, planId, expand, continuationToken);
395
+ const accessToken = await tokenProvider();
396
+ const params = new URLSearchParams({ "api-version": apiVersion, "expand": "children" });
397
+ if (continuationToken)
398
+ params.append("continuationToken", continuationToken);
399
+ const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans/${planId}/Suites?${params.toString()}`;
400
+ const headers = {
401
+ Authorization: `Bearer ${accessToken}`,
402
+ };
403
+ const userAgent = userAgentProvider?.();
404
+ if (userAgent) {
405
+ headers["User-Agent"] = userAgent;
406
+ }
407
+ const response = await fetch(url, {
408
+ method: "GET",
409
+ headers,
410
+ });
411
+ if (!response.ok) {
412
+ const errorText = await response.text();
413
+ throw new Error(`Failed to list test suites (${response.status}): ${errorText}`);
414
+ }
415
+ const body = await response.json();
416
+ const testSuites = body.value ?? [];
417
+ const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
340
418
  // The API returns a flat list where the root suite is first, followed by all nested suites
341
419
  // We need to build a proper hierarchy by creating a map and assembling the tree
342
420
  // Create a map of all suites by ID for quick lookup
@@ -373,7 +451,13 @@ function configureTestPlanTools(server, _, connectionProvider) {
373
451
  }
374
452
  return cleaned;
375
453
  };
376
- const result = roots.map((root) => cleanSuite(root));
454
+ const cleanedSuites = roots.map((root) => cleanSuite(root));
455
+ const result = {
456
+ testSuites: cleanedSuites,
457
+ };
458
+ if (nextToken) {
459
+ result.continuationToken = nextToken;
460
+ }
377
461
  return {
378
462
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
379
463
  };
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
4
  import { apiVersion } from "../utils.js";
5
+ import { createExternalContentResponse } from "../shared/content-safety.js";
5
6
  const WIKI_TOOLS = {
6
7
  list_wikis: "wiki_list_wikis",
7
8
  get_wiki: "wiki_get_wiki",
@@ -59,9 +60,9 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
59
60
  server.tool(WIKI_TOOLS.list_wiki_pages, "Retrieve a list of wiki pages for a specific wiki and project.", {
60
61
  wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
61
62
  project: z.string().describe("The project name or ID where the wiki is located."),
62
- top: z.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
63
+ top: z.coerce.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
63
64
  continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages."),
64
- pageViewsForDays: z.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
65
+ pageViewsForDays: z.coerce.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
65
66
  }, async ({ wikiIdentifier, project, top = 20, continuationToken, pageViewsForDays }) => {
66
67
  try {
67
68
  const connection = await connectionProvider();
@@ -111,7 +112,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
111
112
  if (recursionLevel) {
112
113
  params.append("recursionLevel", recursionLevel);
113
114
  }
114
- const url = `${baseUrl}/${project}/_apis/wiki/wikis/${wikiIdentifier}/pages?${params.toString()}`;
115
+ const url = `${baseUrl}/${encodeURIComponent(project)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?${params.toString()}`;
115
116
  const response = await fetch(url, {
116
117
  headers: {
117
118
  "Authorization": `Bearer ${accessToken}`,
@@ -173,7 +174,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
173
174
  try {
174
175
  const accessToken = await tokenProvider();
175
176
  const baseUrl = connection.serverUrl.replace(/\/$/, "");
176
- const restUrl = `${baseUrl}/${resolvedProject}/_apis/wiki/wikis/${resolvedWiki}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
177
+ const restUrl = `${baseUrl}/${encodeURIComponent(resolvedProject)}/_apis/wiki/wikis/${encodeURIComponent(resolvedWiki)}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
177
178
  const resp = await fetch(restUrl, {
178
179
  headers: {
179
180
  "Authorization": `Bearer ${accessToken}`,
@@ -209,7 +210,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
209
210
  }
210
211
  pageContent = await streamToString(stream);
211
212
  }
212
- return { content: [{ type: "text", text: JSON.stringify(pageContent, null, 2) }] };
213
+ return createExternalContentResponse(pageContent, "wiki page");
213
214
  }
214
215
  catch (error) {
215
216
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -236,7 +237,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
236
237
  // Build the URL for the wiki page API with version descriptor
237
238
  const baseUrl = connection.serverUrl;
238
239
  const projectParam = project || "";
239
- const url = `${baseUrl}/${projectParam}/_apis/wiki/wikis/${wikiIdentifier}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
240
+ const url = `${baseUrl}/${encodeURIComponent(projectParam)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
240
241
  // First, try to create a new page (PUT without ETag)
241
242
  try {
242
243
  const createResponse = await fetch(url, {