@azure-devops/mcp 2.5.0-nightly.20260407 → 2.5.0-nightly.20260409

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.
@@ -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) {
@@ -258,13 +288,41 @@ function configureTestPlanTools(server, _, connectionProvider) {
258
288
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
259
289
  planid: z.coerce.number().min(1).describe("The ID of the test plan."),
260
290
  suiteid: z.coerce.number().min(1).describe("The ID of the test suite."),
261
- }, async ({ project, planid, suiteid }) => {
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) {
@@ -334,9 +392,29 @@ function configureTestPlanTools(server, _, connectionProvider) {
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
  };
@@ -4,6 +4,8 @@ import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackin
4
4
  import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
5
5
  import { z } from "zod";
6
6
  import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
7
+ import { elicitProject } from "../shared/elicitations.js";
8
+ import { createExternalContentResponse } from "../shared/content-safety.js";
7
9
  const WORKITEM_TOOLS = {
8
10
  my_work_items: "wit_my_work_items",
9
11
  list_backlogs: "wit_list_backlogs",
@@ -27,6 +29,7 @@ const WORKITEM_TOOLS = {
27
29
  work_item_unlink: "wit_work_item_unlink",
28
30
  add_artifact_link: "wit_add_artifact_link",
29
31
  get_work_item_attachment: "wit_get_work_item_attachment",
32
+ query_by_wiql: "wit_query_by_wiql",
30
33
  };
31
34
  function getLinkTypeFromName(name) {
32
35
  switch (name.toLowerCase()) {
@@ -1034,6 +1037,35 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
1034
1037
  };
1035
1038
  }
1036
1039
  });
1040
+ server.tool(WORKITEM_TOOLS.query_by_wiql, "Execute a WIQL (Work Item Query Language) query and return the matching work items. If a project is not specified, you will be prompted to select one.", {
1041
+ wiql: z.string().max(32768).describe('The WIQL query string to execute, e.g., "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.TeamProject] = @project"'),
1042
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
1043
+ team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team context will be used."),
1044
+ timePrecision: z.boolean().optional().describe("Whether to include time precision in date fields. Defaults to false."),
1045
+ top: z.coerce.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
1046
+ }, async ({ wiql, project, team, timePrecision, top }) => {
1047
+ try {
1048
+ const connection = await connectionProvider();
1049
+ let resolvedProject = project;
1050
+ if (!resolvedProject) {
1051
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to run the WIQL query against.");
1052
+ if ("response" in result)
1053
+ return result.response;
1054
+ resolvedProject = result.resolved;
1055
+ }
1056
+ const workItemApi = await connection.getWorkItemTrackingApi();
1057
+ const teamContext = { project: resolvedProject, team };
1058
+ const queryResult = await workItemApi.queryByWiql({ query: wiql }, teamContext, timePrecision, top);
1059
+ return createExternalContentResponse(queryResult, "wiql query results");
1060
+ }
1061
+ catch (error) {
1062
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1063
+ return {
1064
+ content: [{ type: "text", text: `Error executing WIQL query: ${errorMessage}` }],
1065
+ isError: true,
1066
+ };
1067
+ }
1068
+ });
1037
1069
  server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID and return the content as a base64-encoded resource. Useful for viewing images (e.g. screenshots) attached to work items such as bugs.", {
1038
1070
  project: z.string().describe("The name or ID of the Azure DevOps project."),
1039
1071
  attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
package/dist/tools.js CHANGED
@@ -24,7 +24,7 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
24
24
  configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
25
25
  configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
26
26
  configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
27
- configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider));
27
+ configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
28
28
  configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
29
  configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
30
30
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.5.0-nightly.20260407";
1
+ export const packageVersion = "2.5.0-nightly.20260409";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.5.0-nightly.20260407",
3
+ "version": "2.5.0-nightly.20260409",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",