Skip to content

Commit 85ec2f1

Browse files
mkonjikovacMirjana Konjikovac (from Dev Box)danhellem
authored
Add tools for pipeline run REST API endpoints (#460)
Adds support for pipeline run tools that correspond to the following REST API endpoints: https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs?view=azure-devops-rest-7.2. This PR adds support for the following tools: - pipelines_get_run - pipelines_list_runs - pipelines_run_pipeline (this is the new version of build_run_build tool that existed prior to the change) ## GitHub issue number Fixes #432 ## **Associated Risks** Tool build_run_build has been renamed to pipelines_run_pipeline. ## ✅ **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 - [] 🔭 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?** - Change was tested by using the tool on real production data from buildcanary organization. Example of prompts: _"Can you run "Stage Template Pipeline" with ID 5422 from Silviu project where mkonjikovac-test branch is used for repository resource devTemplates? Also, pipelines resource sourcePipeline should point to pipeline run ID 3998856"_ - New unit tests were added via Copilot. --------- Co-authored-by: Mirjana Konjikovac (from Dev Box) <mkonjikovac@microsoft.com> Co-authored-by: Dan Hellem <dahellem@microsoft.com>
1 parent 0468c1c commit 85ec2f1

File tree

3 files changed

+236
-178
lines changed

3 files changed

+236
-178
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ Interact with these Azure DevOps services:
111111
- **build_get_log**: Retrieve the logs for a specific build.
112112
- **build_get_log_by_id**: Get a specific build log by log ID.
113113
- **build_get_changes**: Get the changes associated with a specific build.
114-
- **build_run_build**: Trigger a new build for a specified definition.
115114
- **build_get_status**: Fetch the status of a specific build.
116115
- **build_update_build_stage**: Update the stage of a specific build.
116+
- **pipelines_get_run**: Gets a run for a particular pipeline.
117+
- **pipelines_list_runs**: Gets top 10000 runs for a particular pipeline.
118+
- **pipelines_run_pipeline**: Starts a new run of a pipeline.
117119

118120
### 🚀 Releases
119121

src/tools/builds.ts

Lines changed: 115 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ import { z } from "zod";
1010
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
1111

1212
const BUILD_TOOLS = {
13+
get_builds: "build_get_builds",
14+
get_changes: "build_get_changes",
1315
get_definitions: "build_get_definitions",
1416
get_definition_revisions: "build_get_definition_revisions",
15-
get_builds: "build_get_builds",
1617
get_log: "build_get_log",
1718
get_log_by_id: "build_get_log_by_id",
18-
get_changes: "build_get_changes",
19-
run_build: "build_run_build",
2019
get_status: "build_get_status",
20+
pipelines_get_run: "pipelines_get_run",
21+
pipelines_list_runs: "pipelines_list_runs",
22+
pipelines_run_pipeline: "pipelines_run_pipeline",
2123
update_build_stage: "build_update_build_stage",
2224
};
2325

@@ -258,40 +260,133 @@ function configureBuildTools(server: McpServer, tokenProvider: () => Promise<Acc
258260
);
259261

260262
server.tool(
261-
BUILD_TOOLS.run_build,
262-
"Triggers a new build for a specified definition.",
263+
BUILD_TOOLS.pipelines_get_run,
264+
"Gets a run for a particular pipeline.",
263265
{
264266
project: z.string().describe("Project ID or name to run the build in"),
265-
definitionId: z.number().describe("ID of the build definition to run"),
266-
sourceBranch: z.string().optional().describe("Source branch to run the build from. If not provided, the default branch will be used."),
267-
parameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs"),
267+
pipelineId: z.number().describe("ID of the pipeline to run"),
268+
runId: z.number().describe("ID of the run to get"),
268269
},
269-
async ({ project, definitionId, sourceBranch, parameters }) => {
270+
async ({ project, pipelineId, runId }) => {
271+
const connection = await connectionProvider();
272+
const pipelinesApi = await connection.getPipelinesApi();
273+
const pipelineRun = await pipelinesApi.getRun(project, pipelineId, runId);
274+
275+
return {
276+
content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
277+
};
278+
}
279+
);
280+
281+
server.tool(
282+
BUILD_TOOLS.pipelines_list_runs,
283+
"Gets top 10000 runs for a particular pipeline.",
284+
{
285+
project: z.string().describe("Project ID or name to run the build in"),
286+
pipelineId: z.number().describe("ID of the pipeline to run"),
287+
},
288+
async ({ project, pipelineId }) => {
289+
const connection = await connectionProvider();
290+
const pipelinesApi = await connection.getPipelinesApi();
291+
const pipelineRuns = await pipelinesApi.listRuns(project, pipelineId);
292+
293+
return {
294+
content: [{ type: "text", text: JSON.stringify(pipelineRuns, null, 2) }],
295+
};
296+
}
297+
);
298+
299+
const variableSchema = z.object({
300+
value: z.string().optional(),
301+
isSecret: z.boolean().optional(),
302+
});
303+
304+
const resourcesSchema = z.object({
305+
builds: z
306+
.record(
307+
z.string().describe("Name of the build resource."),
308+
z.object({
309+
version: z.string().optional().describe("Version of the build resource."),
310+
})
311+
)
312+
.optional(),
313+
containers: z
314+
.record(
315+
z.string().describe("Name of the container resource."),
316+
z.object({
317+
version: z.string().optional().describe("Version of the container resource."),
318+
})
319+
)
320+
.optional(),
321+
packages: z
322+
.record(
323+
z.string().describe("Name of the package resource."),
324+
z.object({
325+
version: z.string().optional().describe("Version of the package resource."),
326+
})
327+
)
328+
.optional(),
329+
pipelines: z.record(
330+
z.string().describe("Name of the pipeline resource."),
331+
z.object({
332+
runId: z.number().describe("Id of the source pipeline run that triggered or is referenced by this pipeline run."),
333+
version: z.string().optional().describe("Version of the source pipeline run."),
334+
})
335+
),
336+
repositories: z
337+
.record(
338+
z.string().describe("Name of the repository resource."),
339+
z.object({
340+
refName: z.string().describe("Reference name, e.g., refs/heads/main."),
341+
token: z.string().optional(),
342+
tokenType: z.string().optional(),
343+
version: z.string().optional().describe("Version of the repository resource, git commit sha."),
344+
})
345+
)
346+
.optional(),
347+
});
348+
349+
server.tool(
350+
BUILD_TOOLS.pipelines_run_pipeline,
351+
"Starts a new run of a pipeline.",
352+
{
353+
project: z.string().describe("Project ID or name to run the build in"),
354+
pipelineId: z.number().describe("ID of the pipeline to run"),
355+
pipelineVersion: z.number().optional().describe("Version of the pipeline to run. If not provided, the latest version will be used."),
356+
previewRun: z.boolean().optional().describe("If true, returns the final YAML document after parsing templates without creating a new run."),
357+
resources: resourcesSchema.optional().describe("A dictionary of resources to pass to the pipeline."),
358+
stagesToSkip: z.array(z.string()).optional().describe("A list of stages to skip."),
359+
templateParameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs"),
360+
variables: z.record(z.string(), variableSchema).optional().describe("A dictionary of variables to pass to the pipeline."),
361+
yamlOverride: z.string().optional().describe("YAML override for the pipeline run."),
362+
},
363+
async ({ project, pipelineId, pipelineVersion, previewRun, resources, stagesToSkip, templateParameters, variables, yamlOverride }) => {
364+
if (!previewRun && yamlOverride) {
365+
throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
366+
}
367+
270368
const connection = await connectionProvider();
271-
const buildApi = await connection.getBuildApi();
272369
const pipelinesApi = await connection.getPipelinesApi();
273-
const definition = await buildApi.getDefinition(project, definitionId);
274370
const runRequest = {
371+
previewRun: previewRun,
275372
resources: {
276-
repositories: {
277-
self: {
278-
refName: sourceBranch || definition.repository?.defaultBranch || "refs/heads/main",
279-
},
280-
},
373+
...resources,
281374
},
282-
templateParameters: parameters,
375+
stagesToSkip: stagesToSkip,
376+
templateParameters: templateParameters,
377+
variables: variables,
378+
yamlOverride: yamlOverride,
283379
};
284380

285-
const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, definitionId);
381+
const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, pipelineId, pipelineVersion);
286382
const queuedBuild = { id: pipelineRun.id };
287383
const buildId = queuedBuild.id;
288384
if (buildId === undefined) {
289385
throw new Error("Failed to get build ID from pipeline run");
290386
}
291387

292-
const buildReport = await buildApi.getBuildReport(project, buildId);
293388
return {
294-
content: [{ type: "text", text: JSON.stringify(buildReport, null, 2) }],
389+
content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
295390
};
296391
}
297392
);

0 commit comments

Comments
 (0)