33
44import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
55import { 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" ;
77import { z } from "zod" ;
8+ import { apiVersion } from "../utils.js" ;
89
910const 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