Skip to content

Commit bf6d04a

Browse files
authored
fix: Update field and expand parameter on get_work_item (#1144)
This pull request improves the handling of the `fields` and `expand` parameters in the Azure DevOps work item retrieval tool, ensuring they are not used together and clarifying their precedence. It also adds comprehensive tests to verify the new logic. ## 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?** manual testing and updated automated tests
1 parent 6a64721 commit bf6d04a

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

src/tools/work-items.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,25 +265,38 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
265265
{
266266
id: z.coerce.number().min(1).describe("The ID of the work item to retrieve."),
267267
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
268-
fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, all fields will be returned."),
268+
fields: z
269+
.array(z.string())
270+
.optional()
271+
.describe("Optional list of fields to include in the response. If not provided, all fields will be returned. Cannot be used together with the expand parameter."),
269272
asOf: z.coerce.date().optional().describe("Optional date string to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
270273
expand: z
271274
.enum(["all", "fields", "links", "none", "relations"])
272-
.describe("Optional expand parameter to include additional details in the response.")
275+
.describe("Optional expand parameter to include additional details in the response. Cannot be used together with the fields parameter.")
273276
.optional()
274-
.describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Relations can be used to get child workitems. Defaults to 'none'."),
277+
.describe(
278+
"Expand options include 'All', 'Fields', 'Links', 'None', and 'Relations'. Relations can be used to get child workitems. Defaults to 'None'. Cannot be used together with the fields parameter."
279+
),
275280
},
276281
async ({ id, project, fields, asOf, expand }) => {
277282
try {
278283
const connection = await connectionProvider();
279284

280285
let resolvedProject = project;
286+
281287
if (!resolvedProject) {
282288
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
289+
283290
if ("response" in result) return result.response;
284291
resolvedProject = result.resolved;
285292
}
286293

294+
// The Azure DevOps API does not support using expand and fields together.
295+
// When both are provided, prefer fields as it is the more specific selection.
296+
if (fields && fields.length > 0 && expand != null) {
297+
expand = "none";
298+
}
299+
287300
const workItemApi = await connection.getWorkItemTrackingApi();
288301
const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand as unknown as WorkItemExpand, resolvedProject);
289302

test/src/tools/work-items.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,82 @@ describe("configureWorkItemTools", () => {
688688

689689
expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItem], null, 2));
690690
});
691+
692+
it("should call getWorkItem with fields and no expand when fields are provided but expand is empty", async () => {
693+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
694+
695+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_item");
696+
697+
if (!call) throw new Error("wit_get_work_item tool not registered");
698+
const [, , , handler] = call;
699+
700+
(mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(_mockWorkItem);
701+
702+
const params = {
703+
id: 12,
704+
fields: ["System.Title", "System.State"],
705+
asOf: undefined,
706+
expand: undefined,
707+
project: "Contoso",
708+
};
709+
710+
const result = await handler(params);
711+
712+
expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(params.id, params.fields, params.asOf, undefined, params.project);
713+
714+
expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItem, null, 2));
715+
});
716+
717+
it("should call getWorkItem with expand and no fields when expand is provided but fields are empty", async () => {
718+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
719+
720+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_item");
721+
722+
if (!call) throw new Error("wit_get_work_item tool not registered");
723+
const [, , , handler] = call;
724+
725+
(mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(_mockWorkItem);
726+
727+
const params = {
728+
id: 12,
729+
fields: undefined,
730+
asOf: undefined,
731+
expand: "relations",
732+
project: "Contoso",
733+
};
734+
735+
const result = await handler(params);
736+
737+
expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(params.id, params.fields, params.asOf, "relations", params.project);
738+
739+
expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItem, null, 2));
740+
});
741+
742+
it("should override expand to 'none' when both fields and expand are provided", async () => {
743+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
744+
745+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_item");
746+
747+
if (!call) throw new Error("wit_get_work_item tool not registered");
748+
const [, , , handler] = call;
749+
750+
(mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(_mockWorkItem);
751+
752+
const params = {
753+
id: 12,
754+
fields: ["System.Title", "System.State"],
755+
asOf: undefined,
756+
expand: "relations",
757+
project: "Contoso",
758+
};
759+
760+
const result = await handler(params);
761+
762+
// expand should be overridden to "none" because fields takes precedence
763+
expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(params.id, params.fields, params.asOf, "none", params.project);
764+
765+
expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItem, null, 2));
766+
});
691767
});
692768

693769
describe("list_work_item_comments tool", () => {

0 commit comments

Comments
 (0)