Skip to content

Commit 7b082a3

Browse files
authored
Merge branch 'main' into users/danhellem/query-by-wiql-tool-1
2 parents b170aa2 + 511856c commit 7b082a3

4 files changed

Lines changed: 292 additions & 96 deletions

File tree

docs/TOOLSET.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ Adds existing test cases to a test suite.
455455
Gets a list of test cases in the test plan.
456456

457457
- **Required**: `project`, `planid`, `suiteid`
458-
- **Optional**: None
458+
- **Optional**: `continuationToken`
459459

460460
### mcp_ado_testplan_create_test_case
461461

src/tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function configureAllTools(server: McpServer, tokenProvider: () => Promise<strin
3030
configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
3131
configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
3232
configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
33-
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider));
33+
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
3434
configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
3535
configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
3636
}

src/tools/test-plans.ts

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
55
import { WebApi } from "azure-devops-node-api";
6-
import { SuiteExpand, TestPlanCreateParams } from "azure-devops-node-api/interfaces/TestPlanInterfaces.js";
6+
import { TestPlanCreateParams } from "azure-devops-node-api/interfaces/TestPlanInterfaces.js";
77
import { z } from "zod";
8+
import { apiVersion } from "../utils.js";
89

910
const Test_Plan_Tools = {
1011
create_test_plan: "testplan_create_test_plan",
@@ -18,7 +19,7 @@ const Test_Plan_Tools = {
1819
create_test_suite: "testplan_create_test_suite",
1920
};
2021

21-
function configureTestPlanTools(server: McpServer, _: () => Promise<string>, connectionProvider: () => Promise<WebApi>) {
22+
function configureTestPlanTools(server: McpServer, tokenProvider: () => Promise<string>, connectionProvider: () => Promise<WebApi>, userAgentProvider?: () => string) {
2223
server.tool(
2324
Test_Plan_Tools.list_test_plans,
2425
"Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.",
@@ -30,14 +31,45 @@ function configureTestPlanTools(server: McpServer, _: () => Promise<string>, con
3031
},
3132
async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
3233
try {
33-
const owner = ""; //making owner an empty string untill we can figure out how to get owner id
3434
const connection = await connectionProvider();
35-
const testPlanApi = await connection.getTestPlanApi();
35+
const accessToken = await tokenProvider();
36+
const params = new URLSearchParams({ "api-version": apiVersion });
37+
if (filterActivePlans) params.append("filterActivePlans", "true");
38+
if (includePlanDetails) params.append("includePlanDetails", "true");
39+
if (continuationToken) params.append("continuationToken", continuationToken);
40+
const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans?${params.toString()}`;
41+
const headers: Record<string, string> = {
42+
Authorization: `Bearer ${accessToken}`,
43+
};
44+
45+
const userAgent = userAgentProvider?.();
46+
if (userAgent) {
47+
headers["User-Agent"] = userAgent;
48+
}
49+
50+
const response = await fetch(url, {
51+
method: "GET",
52+
headers,
53+
});
54+
55+
if (!response.ok) {
56+
const errorText = await response.text();
57+
throw new Error(`Failed to list test plans (${response.status}): ${errorText}`);
58+
}
59+
60+
const body = await response.json();
61+
const testPlans = body.value ?? [];
62+
const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
3663

37-
const testPlans = await testPlanApi.getTestPlans(project, owner, continuationToken, includePlanDetails, filterActivePlans);
64+
const result: { testPlans: typeof testPlans; continuationToken?: string } = {
65+
testPlans: testPlans,
66+
};
67+
if (nextToken) {
68+
result.continuationToken = nextToken;
69+
}
3870

3971
return {
40-
content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
72+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
4173
};
4274
} catch (error) {
4375
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -331,15 +363,47 @@ function configureTestPlanTools(server: McpServer, _: () => Promise<string>, con
331363
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
332364
planid: z.coerce.number().min(1).describe("The ID of the test plan."),
333365
suiteid: z.coerce.number().min(1).describe("The ID of the test suite."),
366+
continuationToken: z.string().optional().describe("Token to continue fetching test cases from a previous request."),
334367
},
335-
async ({ project, planid, suiteid }) => {
368+
async ({ project, planid, suiteid, continuationToken }) => {
336369
try {
337370
const connection = await connectionProvider();
338-
const coreApi = await connection.getTestPlanApi();
339-
const testcases = await coreApi.getTestCaseList(project, planid, suiteid);
371+
const accessToken = await tokenProvider();
372+
const params = new URLSearchParams({ "api-version": "7.2-preview.3" });
373+
if (continuationToken) params.append("continuationToken", continuationToken);
374+
const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans/${planid}/Suites/${suiteid}/TestCase?${params.toString()}`;
375+
const headers: Record<string, string> = {
376+
Authorization: `Bearer ${accessToken}`,
377+
};
378+
379+
const userAgent = userAgentProvider?.();
380+
if (userAgent) {
381+
headers["User-Agent"] = userAgent;
382+
}
383+
384+
const response = await fetch(url, {
385+
method: "GET",
386+
headers,
387+
});
388+
389+
if (!response.ok) {
390+
const errorText = await response.text();
391+
throw new Error(`Failed to list test cases (${response.status}): ${errorText}`);
392+
}
393+
394+
const body = await response.json();
395+
const testcases = body.value ?? [];
396+
const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
397+
398+
const result: { testCases: typeof testcases; continuationToken?: string } = {
399+
testCases: testcases,
400+
};
401+
if (nextToken) {
402+
result.continuationToken = nextToken;
403+
}
340404

341405
return {
342-
content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
406+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
343407
};
344408
} catch (error) {
345409
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -427,10 +491,32 @@ function configureTestPlanTools(server: McpServer, _: () => Promise<string>, con
427491
async ({ project, planId, continuationToken }) => {
428492
try {
429493
const connection = await connectionProvider();
430-
const testPlanApi = await connection.getTestPlanApi();
431-
const expand: SuiteExpand = SuiteExpand.Children;
494+
const accessToken = await tokenProvider();
495+
const params = new URLSearchParams({ "api-version": apiVersion, "expand": "children" });
496+
if (continuationToken) params.append("continuationToken", continuationToken);
497+
const url = `${connection.serverUrl}/${encodeURIComponent(project)}/_apis/testplan/Plans/${planId}/Suites?${params.toString()}`;
498+
const headers: Record<string, string> = {
499+
Authorization: `Bearer ${accessToken}`,
500+
};
432501

433-
const testSuites = await testPlanApi.getTestSuitesForPlan(project, planId, expand, continuationToken);
502+
const userAgent = userAgentProvider?.();
503+
if (userAgent) {
504+
headers["User-Agent"] = userAgent;
505+
}
506+
507+
const response = await fetch(url, {
508+
method: "GET",
509+
headers,
510+
});
511+
512+
if (!response.ok) {
513+
const errorText = await response.text();
514+
throw new Error(`Failed to list test suites (${response.status}): ${errorText}`);
515+
}
516+
517+
const body = await response.json();
518+
const testSuites = body.value ?? [];
519+
const nextToken = response.headers.get("x-ms-continuationtoken") ?? undefined;
434520

435521
// The API returns a flat list where the root suite is first, followed by all nested suites
436522
// We need to build a proper hierarchy by creating a map and assembling the tree
@@ -471,7 +557,14 @@ function configureTestPlanTools(server: McpServer, _: () => Promise<string>, con
471557
return cleaned;
472558
};
473559

474-
const result = roots.map((root: any) => cleanSuite(root));
560+
const cleanedSuites = roots.map((root: any) => cleanSuite(root));
561+
562+
const result: { testSuites: typeof cleanedSuites; continuationToken?: string } = {
563+
testSuites: cleanedSuites,
564+
};
565+
if (nextToken) {
566+
result.continuationToken = nextToken;
567+
}
475568

476569
return {
477570
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],

0 commit comments

Comments
 (0)