Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/tools/workitems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,22 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
async ({ project, ids }) => {
const connection = await connectionProvider();
const workItemApi = await connection.getWorkItemTrackingApi();
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank"];
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields }, project);

// Format the assignedTo field to include displayName and uniqueName
// Removing the identity object as the response. It's too much and not needed
if (workitems && Array.isArray(workitems)) {
workitems.forEach((item) => {
if (item.fields && item.fields["System.AssignedTo"] && typeof item.fields["System.AssignedTo"] === "object") {
const assignedTo = item.fields["System.AssignedTo"];
const name = assignedTo.displayName || "";
const email = assignedTo.uniqueName || "";
item.fields["System.AssignedTo"] = `${name} <${email}>`.trim();
}
});
}

return {
content: [{ type: "text", text: JSON.stringify(workitems, null, 2) }],
};
Expand Down
213 changes: 212 additions & 1 deletion test/src/tools/workitems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,224 @@ describe("configureWorkItemTools", () => {
expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith(
{
ids: params.ids,
fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank"],
fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"],
},
params.project
);

expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItems], null, 2));
});

it("should transform System.AssignedTo object to formatted string", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

// Mock work items with System.AssignedTo as objects
const mockWorkItemsWithAssignedTo = [
{
id: 297,
fields: {
"System.Id": 297,
"System.WorkItemType": "Bug",
"System.Title": "Test Bug",
"System.AssignedTo": {
displayName: "John Doe",
uniqueName: "[email protected]",
id: "12345",
},
},
},
{
id: 298,
fields: {
"System.Id": 298,
"System.WorkItemType": "User Story",
"System.Title": "Test Story",
"System.AssignedTo": {
displayName: "Jane Smith",
uniqueName: "[email protected]",
id: "67890",
},
},
},
];

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithAssignedTo);

const params = {
ids: [297, 298],
project: "Contoso",
};

const result = await handler(params);

// Parse the returned JSON to verify transformation
const resultData = JSON.parse(result.content[0].text);

expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <[email protected]>");
expect(resultData[1].fields["System.AssignedTo"]).toBe("Jane Smith <[email protected]>");
});

it("should handle System.AssignedTo with only displayName", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

const mockWorkItemsWithPartialAssignedTo = [
{
id: 297,
fields: {
"System.Id": 297,
"System.WorkItemType": "Bug",
"System.Title": "Test Bug",
"System.AssignedTo": {
displayName: "John Doe",
id: "12345",
},
},
},
];

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo);

const params = {
ids: [297],
project: "Contoso",
};

const result = await handler(params);

const resultData = JSON.parse(result.content[0].text);
expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <>");
});

it("should handle System.AssignedTo with only uniqueName", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

const mockWorkItemsWithPartialAssignedTo = [
{
id: 297,
fields: {
"System.Id": 297,
"System.WorkItemType": "Bug",
"System.Title": "Test Bug",
"System.AssignedTo": {
uniqueName: "[email protected]",
id: "12345",
},
},
},
];

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo);

const params = {
ids: [297],
project: "Contoso",
};

const result = await handler(params);

const resultData = JSON.parse(result.content[0].text);
expect(resultData[0].fields["System.AssignedTo"]).toBe("<[email protected]>");
});

it("should not transform System.AssignedTo if it's not an object", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

const mockWorkItemsWithStringAssignedTo = [
{
id: 297,
fields: {
"System.Id": 297,
"System.WorkItemType": "Bug",
"System.Title": "Test Bug",
"System.AssignedTo": "Already a string",
},
},
];

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithStringAssignedTo);

const params = {
ids: [297],
project: "Contoso",
};

const result = await handler(params);

const resultData = JSON.parse(result.content[0].text);
expect(resultData[0].fields["System.AssignedTo"]).toBe("Already a string");
});

it("should handle work items without System.AssignedTo field", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

const mockWorkItemsWithoutAssignedTo = [
{
id: 297,
fields: {
"System.Id": 297,
"System.WorkItemType": "Bug",
"System.Title": "Test Bug",
},
},
];

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithoutAssignedTo);

const params = {
ids: [297],
project: "Contoso",
};

const result = await handler(params);

const resultData = JSON.parse(result.content[0].text);
expect(resultData[0].fields["System.AssignedTo"]).toBeUndefined();
});

it("should handle null or undefined workitems response", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids");

if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered");
const [, , , handler] = call;

(mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(null);

const params = {
ids: [297],
project: "Contoso",
};

const result = await handler(params);

expect(result.content[0].text).toBe(JSON.stringify(null, null, 2));
});
});

describe("get_work_item tool", () => {
Expand Down