Skip to content

Commit 2ffa682

Browse files
authored
fix: update queryOrder parameters to use SDK enum keys in configurePi… (#1657)
This pull request improves the handling and validation of the `queryOrder` parameter for build and definition queries in the pipelines tooling. Instead of accepting any string, the code now enforces that only valid Azure DevOps SDK enum values are allowed. The tests have also been updated to verify this stricter validation and correct enum mapping. **Parameter validation and typing improvements:** * Changed the `queryOrder` parameter in both build and definition query schemas to use a `z.enum` of the actual SDK enum keys, ensuring only valid values are accepted. (`src/tools/pipelines.ts`) [[1]](diffhunk://#diff-61ea19714cb175a5845bd938c66c5f6f0226448eb281419059c7c3ef889fb29eR9-R11) [[2]](diffhunk://#diff-61ea19714cb175a5845bd938c66c5f6f0226448eb281419059c7c3ef889fb29eL172-R175) [[3]](diffhunk://#diff-61ea19714cb175a5845bd938c66c5f6f0226448eb281419059c7c3ef889fb29eL317-R320) **Testing enhancements:** * Added and updated tests to verify that the `queryOrder` parameter only accepts valid enum keys (case-sensitive), and that the correct SDK enum values are used when invoking the Azure DevOps API. (`test/src/tools/pipelines.test.ts`) [[1]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbL7-R7) [[2]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbR49-R63) [[3]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbR424) [[4]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbL419-R435) [[5]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbR779) [[6]](diffhunk://#diff-fb7cbb4ca60175990b48782444c077c05ca3c38613aa12b0d8169ed3da0eb6cbL785-R802) ## GitHub issue number #1654 ## **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 tested, confirmed with copilot, updated automated tests
1 parent 52072ff commit 2ffa682

2 files changed

Lines changed: 25 additions & 5 deletions

File tree

‎src/tools/pipelines.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
66
import { WebApi } from "azure-devops-node-api";
77
import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
88
import { z } from "zod";
9+
10+
const buildQueryOrderKeys = getEnumKeys(BuildQueryOrder) as [string, ...string[]];
11+
const definitionQueryOrderKeys = getEnumKeys(DefinitionQueryOrder) as [string, ...string[]];
912
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
1013
import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
1114
import { mkdirSync, createWriteStream } from "fs";
@@ -169,7 +172,7 @@ function configurePipelineTools(server: McpServer, tokenProvider: () => Promise<
169172
continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list, get_changes."),
170173
maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition. Used for: list."),
171174
deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum). Used for: list."),
172-
queryOrder: z.string().optional().describe("Order in which builds are returned (BuildQueryOrder values). Used for: list."),
175+
queryOrder: z.enum(buildQueryOrderKeys).optional().describe("Order in which builds are returned. Used for: list."),
173176
branchName: z.string().optional().describe("Branch name to filter builds. Used for: list."),
174177
buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of specific build IDs to retrieve. Used for: list."),
175178
repositoryId: z.string().optional().describe("Repository ID to filter builds. Used for: list."),
@@ -314,7 +317,7 @@ function configurePipelineTools(server: McpServer, tokenProvider: () => Promise<
314317
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter definitions. Used for: list."),
315318
name: z.string().optional().describe("Name filter for build definitions. Used for: list."),
316319
path: z.string().optional().describe("Path filter for build definitions. Used for: list."),
317-
queryOrder: z.string().optional().describe("Order in which definitions are returned (DefinitionQueryOrder values). Used for: list."),
320+
queryOrder: z.enum(definitionQueryOrderKeys).optional().describe("Order in which definitions are returned. Used for: list."),
318321
top: z.number().optional().describe("Maximum number of definitions to return. Used for: list."),
319322
continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list."),
320323
minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter definitions. Used for: list."),

‎test/src/tools/pipelines.test.ts‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { describe, expect, it, beforeEach } from "@jest/globals";
55
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
66
import { WebApi } from "azure-devops-node-api";
7-
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
7+
import { BuildQueryOrder, DefinitionQueryOrder, StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
88
import { configurePipelineTools, runPipeline as runPipelineAction, createPipeline as createPipelineAction, updateBuildStage as updateBuildStageAction } from "../../../src/tools/pipelines";
99
import { apiVersion } from "../../../src/utils.js";
1010
import { mockUpdateBuildStageResponse, mockMultipleArtifacts, mockArtifact } from "../../mocks/pipelines";
@@ -46,6 +46,21 @@ describe("configurePipelineTools", () => {
4646
configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider);
4747
expect(server.tool as jest.Mock).toHaveBeenCalled();
4848
});
49+
50+
it("registers queryOrder parameters as SDK enum keys", () => {
51+
configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider);
52+
const buildCall = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "pipelines_build");
53+
const definitionCall = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "pipelines_definition");
54+
if (!buildCall || !definitionCall) throw new Error("pipeline tools not registered");
55+
56+
const buildQueryOrder = buildCall[2].queryOrder;
57+
expect(buildQueryOrder.safeParse("FinishTimeAscending").success).toBe(true);
58+
expect(buildQueryOrder.safeParse("finishTimeAscending").success).toBe(false);
59+
60+
const definitionQueryOrder = definitionCall[2].queryOrder;
61+
expect(definitionQueryOrder.safeParse("LastModifiedDescending").success).toBe(true);
62+
expect(definitionQueryOrder.safeParse("lastModifiedDescending").success).toBe(false);
63+
});
4964
});
5065

5166
describe("update_build_stage tool", () => {
@@ -406,6 +421,7 @@ describe("configurePipelineTools", () => {
406421
repositoryId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
407422
repositoryType: "TfsGit" as const,
408423
name: "test-build",
424+
queryOrder: "LastModifiedDescending",
409425
top: 10,
410426
};
411427

@@ -416,7 +432,7 @@ describe("configurePipelineTools", () => {
416432
"test-build",
417433
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
418434
"TfsGit",
419-
undefined, // queryOrder
435+
DefinitionQueryOrder.LastModifiedDescending,
420436
10, // top
421437
undefined, // continuationToken
422438
undefined, // minMetricsTime
@@ -760,6 +776,7 @@ describe("configurePipelineTools", () => {
760776
project: "test-project",
761777
definitions: [1, 2],
762778
top: 5,
779+
queryOrder: "FinishTimeAscending",
763780
branchName: "refs/heads/main",
764781
};
765782

@@ -782,7 +799,7 @@ describe("configurePipelineTools", () => {
782799
undefined, // continuationToken
783800
undefined, // maxBuildsPerDefinition
784801
undefined, // deletedFilter
785-
undefined, // queryOrder (default BuildQueryOrder.QueueTimeDescending)
802+
BuildQueryOrder.FinishTimeAscending,
786803
"refs/heads/main", // branchName
787804
undefined, // buildIds
788805
undefined, // repositoryId

0 commit comments

Comments
 (0)