Skip to content

Commit 4171bf7

Browse files
authored
refactor: replace getRootNodes with getClassificationNodes (#671)
After further testing, the current implementation was wrong. I was calling the wrong endpoint. Fixed to use correct endpoint ## GitHub issue number N/A ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Tested manually against multiple levels. Fixed tests.
1 parent f636f7c commit 4171bf7

File tree

2 files changed

+32
-57
lines changed

2 files changed

+32
-57
lines changed

src/tools/work.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -118,47 +118,27 @@ function configureWorkTools(server: McpServer, _: () => Promise<string>, connect
118118
try {
119119
const connection = await connectionProvider();
120120
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
121-
const results = [];
122121

123122
if (depth === undefined) {
124123
depth = 1;
125124
}
126125

127-
// Get all root nodes, then filter for the root node with structureType "iteration"
128-
const rootNodes = await workItemTrackingApi.getRootNodes(project, depth);
129-
const iterationRoot = Array.isArray(rootNodes) ? rootNodes.find((node) => node.structureType === TreeNodeStructureType.Iteration) : undefined;
130-
131-
if (iterationRoot) {
132-
// Only return the root and its children (if any)
133-
results.push({
134-
id: iterationRoot.id,
135-
identifier: iterationRoot.identifier,
136-
name: iterationRoot.name,
137-
structureType: iterationRoot.structureType,
138-
hasChildren: iterationRoot.hasChildren,
139-
path: iterationRoot.path,
140-
url: iterationRoot.url,
141-
children: iterationRoot.children
142-
? iterationRoot.children.map((child) => ({
143-
id: child.id,
144-
identifier: child.identifier,
145-
name: child.name,
146-
structureType: child.structureType,
147-
hasChildren: child.hasChildren,
148-
path: child.path,
149-
url: child.url,
150-
attributes: child.attributes,
151-
}))
152-
: [],
153-
});
126+
const results = await workItemTrackingApi.getClassificationNodes(project, [], depth);
127+
128+
// Handle null or undefined results
129+
if (!results) {
130+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
154131
}
155132

156-
if (results.length === 0) {
133+
// Filter out items with structureType=0 (Area nodes), only keep structureType=1 (Iteration nodes)
134+
const filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
135+
136+
if (filteredResults.length === 0) {
157137
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
158138
}
159139

160140
return {
161-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
141+
content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
162142
};
163143
} catch (error) {
164144
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";

test/src/tools/work.test.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface WorkApiMock {
2020

2121
interface WorkItemTrackingApiMock {
2222
createOrUpdateClassificationNode: jest.Mock;
23-
getRootNodes: jest.Mock;
23+
getClassificationNodes: jest.Mock;
2424
}
2525

2626
describe("configureWorkTools", () => {
@@ -45,7 +45,7 @@ describe("configureWorkTools", () => {
4545

4646
mockWorkItemTrackingApi = {
4747
createOrUpdateClassificationNode: jest.fn(),
48-
getRootNodes: jest.fn(),
48+
getClassificationNodes: jest.fn(),
4949
};
5050

5151
mockConnection = {
@@ -181,14 +181,14 @@ describe("configureWorkTools", () => {
181181
});
182182

183183
describe("list_iterations tool", () => {
184-
it("should call getRootNodes API with the correct parameters and return the expected result", async () => {
184+
it("should call getClassificationNodes API with the correct parameters and return the expected result", async () => {
185185
configureWorkTools(server, tokenProvider, connectionProvider);
186186

187187
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "work_list_iterations");
188188
if (!call) throw new Error("work_list_iterations tool not registered");
189189
const [, , , handler] = call;
190190

191-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
191+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
192192
{
193193
id: 126391,
194194
identifier: "a5c68379-3258-4d62-971c-71c1c459336e",
@@ -245,7 +245,7 @@ describe("configureWorkTools", () => {
245245

246246
const result = await handler(params);
247247

248-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 2);
248+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 2);
249249

250250
const expectedResult = [
251251
{
@@ -297,7 +297,7 @@ describe("configureWorkTools", () => {
297297
if (!call) throw new Error("work_list_iterations tool not registered");
298298
const [, , , handler] = call;
299299

300-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
300+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
301301
{
302302
id: 126392,
303303
identifier: "b6d79480-4359-5e73-a82d-f7cg3575e447",
@@ -316,7 +316,7 @@ describe("configureWorkTools", () => {
316316

317317
const result = await handler(params);
318318

319-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
319+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
320320

321321
const expectedResult = [
322322
{
@@ -341,7 +341,7 @@ describe("configureWorkTools", () => {
341341
if (!call) throw new Error("work_list_iterations tool not registered");
342342
const [, , , handler] = call;
343343

344-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
344+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
345345
{
346346
id: 126391,
347347
identifier: "a5c68379-3258-4d62-971c-71c1c459336e",
@@ -371,7 +371,7 @@ describe("configureWorkTools", () => {
371371

372372
const result = await handler(params);
373373

374-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
374+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
375375

376376
// Should only return the iteration node, filtering out the area node
377377
const expectedResult = [
@@ -397,7 +397,7 @@ describe("configureWorkTools", () => {
397397
if (!call) throw new Error("work_list_iterations tool not registered");
398398
const [, , , handler] = call;
399399

400-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
400+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
401401
{
402402
id: 126391,
403403
identifier: "a5c68379-3258-4d62-971c-71c1c459336e",
@@ -417,7 +417,7 @@ describe("configureWorkTools", () => {
417417

418418
const result = await handler(params);
419419

420-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
420+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
421421
expect(result.isError).toBe(true);
422422
expect(result.content[0].text).toBe("No iterations were found");
423423
});
@@ -429,7 +429,7 @@ describe("configureWorkTools", () => {
429429
if (!call) throw new Error("work_list_iterations tool not registered");
430430
const [, , , handler] = call;
431431

432-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([]);
432+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([]);
433433

434434
const params = {
435435
project: "Fabrikam",
@@ -438,7 +438,7 @@ describe("configureWorkTools", () => {
438438

439439
const result = await handler(params);
440440

441-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
441+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
442442
expect(result.isError).toBe(true);
443443
expect(result.content[0].text).toBe("No iterations were found");
444444
});
@@ -450,7 +450,7 @@ describe("configureWorkTools", () => {
450450
if (!call) throw new Error("work_list_iterations tool not registered");
451451
const [, , , handler] = call;
452452

453-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue(null);
453+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue(null);
454454

455455
const params = {
456456
project: "Fabrikam",
@@ -459,7 +459,7 @@ describe("configureWorkTools", () => {
459459

460460
const result = await handler(params);
461461

462-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
462+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
463463
expect(result.isError).toBe(true);
464464
expect(result.content[0].text).toBe("No iterations were found");
465465
});
@@ -471,7 +471,7 @@ describe("configureWorkTools", () => {
471471
if (!call) throw new Error("work_list_iterations tool not registered");
472472
const [, , , handler] = call;
473473

474-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
474+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
475475
{
476476
id: 126392,
477477
identifier: "b6d79480-4359-5e73-a82d-f7cg3575e447",
@@ -491,7 +491,7 @@ describe("configureWorkTools", () => {
491491

492492
const result = await handler(params);
493493

494-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalledWith("Fabrikam", 1);
494+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalledWith("Fabrikam", [], 1);
495495

496496
const expectedResult = [
497497
{
@@ -502,7 +502,6 @@ describe("configureWorkTools", () => {
502502
hasChildren: false,
503503
path: "\\fabrikam\\iteration",
504504
url: "https://dev.azure.com/fabrikam/_apis/wit/classificationNodes/Iterations",
505-
children: [],
506505
},
507506
];
508507

@@ -517,7 +516,7 @@ describe("configureWorkTools", () => {
517516
const [, , , handler] = call;
518517

519518
const testError = new Error("Failed to retrieve iterations");
520-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockRejectedValue(testError);
519+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockRejectedValue(testError);
521520

522521
const params = {
523522
project: "Fabrikam",
@@ -526,7 +525,7 @@ describe("configureWorkTools", () => {
526525

527526
const result = await handler(params);
528527

529-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalled();
528+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalled();
530529
expect(result.isError).toBe(true);
531530
expect(result.content[0].text).toContain("Error fetching iterations: Failed to retrieve iterations");
532531
});
@@ -538,7 +537,7 @@ describe("configureWorkTools", () => {
538537
if (!call) throw new Error("work_list_iterations tool not registered");
539538
const [, , , handler] = call;
540539

541-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockRejectedValue("string error");
540+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockRejectedValue("string error");
542541

543542
const params = {
544543
project: "Fabrikam",
@@ -547,7 +546,7 @@ describe("configureWorkTools", () => {
547546

548547
const result = await handler(params);
549548

550-
expect(mockWorkItemTrackingApi.getRootNodes).toHaveBeenCalled();
549+
expect(mockWorkItemTrackingApi.getClassificationNodes).toHaveBeenCalled();
551550
expect(result.isError).toBe(true);
552551
expect(result.content[0].text).toContain("Error fetching iterations: Unknown error occurred");
553552
});
@@ -559,7 +558,7 @@ describe("configureWorkTools", () => {
559558
if (!call) throw new Error("work_list_iterations tool not registered");
560559
const [, , , handler] = call;
561560

562-
(mockWorkItemTrackingApi.getRootNodes as jest.Mock).mockResolvedValue([
561+
(mockWorkItemTrackingApi.getClassificationNodes as jest.Mock).mockResolvedValue([
563562
{
564563
id: 126392,
565564
identifier: "b6d79480-4359-5e73-a82d-f7cg3575e447",
@@ -581,7 +580,6 @@ describe("configureWorkTools", () => {
581580
startDate: "2025-01-01T00:00:00Z",
582581
finishDate: "2025-01-14T23:59:59Z",
583582
},
584-
extraProperty: "should be filtered out",
585583
},
586584
],
587585
},
@@ -610,9 +608,6 @@ describe("configureWorkTools", () => {
610608
finishDate: "2025-01-14T23:59:59Z",
611609
},
612610
});
613-
614-
// Verify that extraProperty is not included
615-
expect(parsedResult[0].children[0].extraProperty).toBeUndefined();
616611
});
617612
});
618613

0 commit comments

Comments
 (0)