Skip to content

Commit 44c8428

Browse files
authored
AssignedTo transformation for get work items in batch (#363)
Added a transformation on get work items in batch to only show the AssignedTo value and not a complex object ## GitHub issue number #327 ## **Associated Risks** N/A ## ✅ **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?** Manual tests. Updated automated tests
1 parent 49f5f59 commit 44c8428

File tree

2 files changed

+226
-2
lines changed

2 files changed

+226
-2
lines changed

src/tools/workitems.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,22 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
130130
async ({ project, ids }) => {
131131
const connection = await connectionProvider();
132132
const workItemApi = await connection.getWorkItemTrackingApi();
133-
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank"];
133+
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
134134
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields }, project);
135135

136+
// Format the assignedTo field to include displayName and uniqueName
137+
// Removing the identity object as the response. It's too much and not needed
138+
if (workitems && Array.isArray(workitems)) {
139+
workitems.forEach((item) => {
140+
if (item.fields && item.fields["System.AssignedTo"] && typeof item.fields["System.AssignedTo"] === "object") {
141+
const assignedTo = item.fields["System.AssignedTo"];
142+
const name = assignedTo.displayName || "";
143+
const email = assignedTo.uniqueName || "";
144+
item.fields["System.AssignedTo"] = `${name} <${email}>`.trim();
145+
}
146+
});
147+
}
148+
136149
return {
137150
content: [{ type: "text", text: JSON.stringify(workitems, null, 2) }],
138151
};

test/src/tools/workitems.test.ts

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,224 @@ describe("configureWorkItemTools", () => {
285285
expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith(
286286
{
287287
ids: params.ids,
288-
fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank"],
288+
fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"],
289289
},
290290
params.project
291291
);
292292

293293
expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItems], null, 2));
294294
});
295+
296+
it("should transform System.AssignedTo object to formatted string", async () => {
297+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
298+
299+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
300+
301+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
302+
const [, , , handler] = call;
303+
304+
// Mock work items with System.AssignedTo as objects
305+
const mockWorkItemsWithAssignedTo = [
306+
{
307+
id: 297,
308+
fields: {
309+
"System.Id": 297,
310+
"System.WorkItemType": "Bug",
311+
"System.Title": "Test Bug",
312+
"System.AssignedTo": {
313+
displayName: "John Doe",
314+
uniqueName: "[email protected]",
315+
id: "12345",
316+
},
317+
},
318+
},
319+
{
320+
id: 298,
321+
fields: {
322+
"System.Id": 298,
323+
"System.WorkItemType": "User Story",
324+
"System.Title": "Test Story",
325+
"System.AssignedTo": {
326+
displayName: "Jane Smith",
327+
uniqueName: "[email protected]",
328+
id: "67890",
329+
},
330+
},
331+
},
332+
];
333+
334+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithAssignedTo);
335+
336+
const params = {
337+
ids: [297, 298],
338+
project: "Contoso",
339+
};
340+
341+
const result = await handler(params);
342+
343+
// Parse the returned JSON to verify transformation
344+
const resultData = JSON.parse(result.content[0].text);
345+
346+
expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <[email protected]>");
347+
expect(resultData[1].fields["System.AssignedTo"]).toBe("Jane Smith <[email protected]>");
348+
});
349+
350+
it("should handle System.AssignedTo with only displayName", async () => {
351+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
352+
353+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
354+
355+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
356+
const [, , , handler] = call;
357+
358+
const mockWorkItemsWithPartialAssignedTo = [
359+
{
360+
id: 297,
361+
fields: {
362+
"System.Id": 297,
363+
"System.WorkItemType": "Bug",
364+
"System.Title": "Test Bug",
365+
"System.AssignedTo": {
366+
displayName: "John Doe",
367+
id: "12345",
368+
},
369+
},
370+
},
371+
];
372+
373+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo);
374+
375+
const params = {
376+
ids: [297],
377+
project: "Contoso",
378+
};
379+
380+
const result = await handler(params);
381+
382+
const resultData = JSON.parse(result.content[0].text);
383+
expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <>");
384+
});
385+
386+
it("should handle System.AssignedTo with only uniqueName", async () => {
387+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
388+
389+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
390+
391+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
392+
const [, , , handler] = call;
393+
394+
const mockWorkItemsWithPartialAssignedTo = [
395+
{
396+
id: 297,
397+
fields: {
398+
"System.Id": 297,
399+
"System.WorkItemType": "Bug",
400+
"System.Title": "Test Bug",
401+
"System.AssignedTo": {
402+
uniqueName: "[email protected]",
403+
id: "12345",
404+
},
405+
},
406+
},
407+
];
408+
409+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo);
410+
411+
const params = {
412+
ids: [297],
413+
project: "Contoso",
414+
};
415+
416+
const result = await handler(params);
417+
418+
const resultData = JSON.parse(result.content[0].text);
419+
expect(resultData[0].fields["System.AssignedTo"]).toBe("<[email protected]>");
420+
});
421+
422+
it("should not transform System.AssignedTo if it's not an object", async () => {
423+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
424+
425+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
426+
427+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
428+
const [, , , handler] = call;
429+
430+
const mockWorkItemsWithStringAssignedTo = [
431+
{
432+
id: 297,
433+
fields: {
434+
"System.Id": 297,
435+
"System.WorkItemType": "Bug",
436+
"System.Title": "Test Bug",
437+
"System.AssignedTo": "Already a string",
438+
},
439+
},
440+
];
441+
442+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithStringAssignedTo);
443+
444+
const params = {
445+
ids: [297],
446+
project: "Contoso",
447+
};
448+
449+
const result = await handler(params);
450+
451+
const resultData = JSON.parse(result.content[0].text);
452+
expect(resultData[0].fields["System.AssignedTo"]).toBe("Already a string");
453+
});
454+
455+
it("should handle work items without System.AssignedTo field", async () => {
456+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
457+
458+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
459+
460+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
461+
const [, , , handler] = call;
462+
463+
const mockWorkItemsWithoutAssignedTo = [
464+
{
465+
id: 297,
466+
fields: {
467+
"System.Id": 297,
468+
"System.WorkItemType": "Bug",
469+
"System.Title": "Test Bug",
470+
},
471+
},
472+
];
473+
474+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithoutAssignedTo);
475+
476+
const params = {
477+
ids: [297],
478+
project: "Contoso",
479+
};
480+
481+
const result = await handler(params);
482+
483+
const resultData = JSON.parse(result.content[0].text);
484+
expect(resultData[0].fields["System.AssignedTo"]).toBeUndefined();
485+
});
486+
487+
it("should handle null or undefined workitems response", async () => {
488+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
489+
490+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");
491+
492+
if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
493+
const [, , , handler] = call;
494+
495+
(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(null);
496+
497+
const params = {
498+
ids: [297],
499+
project: "Contoso",
500+
};
501+
502+
const result = await handler(params);
503+
504+
expect(result.content[0].text).toBe(JSON.stringify(null, null, 2));
505+
});
295506
});
296507

297508
describe("get_work_item tool", () => {

0 commit comments

Comments
 (0)