@azure-devops/mcp 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,325 +8,88 @@ import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfa
8
8
  import { mkdirSync, createWriteStream } from "fs";
9
9
  import { createExternalContentResponse } from "../shared/content-safety.js";
10
10
  import { join, posix, resolve, win32 } from "path";
11
- const PIPELINE_TOOLS = {
12
- pipelines_get_builds: "pipelines_get_builds",
13
- pipelines_get_build_changes: "pipelines_get_build_changes",
14
- pipelines_get_build_definitions: "pipelines_get_build_definitions",
15
- pipelines_get_build_definition_revisions: "pipelines_get_build_definition_revisions",
16
- pipelines_get_build_log: "pipelines_get_build_log",
17
- pipelines_get_build_log_by_id: "pipelines_get_build_log_by_id",
18
- pipelines_get_build_status: "pipelines_get_build_status",
19
- pipelines_update_build_stage: "pipelines_update_build_stage",
20
- pipelines_create_pipeline: "pipelines_create_pipeline",
21
- pipelines_get_run: "pipelines_get_run",
22
- pipelines_list_runs: "pipelines_list_runs",
23
- pipelines_run_pipeline: "pipelines_run_pipeline",
24
- pipelines_list_artifacts: "pipelines_list_artifacts",
25
- pipelines_download_artifact: "pipelines_download_artifact",
26
- };
27
- function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
28
- server.tool(PIPELINE_TOOLS.pipelines_get_build_definitions, "Retrieves a list of build definitions for a given project.", {
29
- project: z.string().describe("Project ID or name to get build definitions for"),
30
- repositoryId: z
31
- .string()
32
- .optional()
33
- .describe("Repository ID to filter build definitions. Can be a GUID or a repository name; when a name is provided, it is auto-resolved to the repository GUID using the project parameter (Azure Repos / TfsGit only)."),
34
- repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter build definitions"),
35
- name: z.string().optional().describe("Name of the build definition to filter"),
36
- path: z.string().optional().describe("Path of the build definition to filter"),
37
- queryOrder: z
38
- .enum(getEnumKeys(DefinitionQueryOrder))
39
- .optional()
40
- .describe("Order in which build definitions are returned"),
41
- top: z.number().optional().describe("Maximum number of build definitions to return"),
42
- continuationToken: z.string().optional().describe("Token for continuing paged results"),
43
- minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter build definitions"),
44
- definitionIds: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter"),
45
- builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date"),
46
- notBuiltAfter: z.coerce.date().optional().describe("Return definitions that do not have builds after this date"),
47
- includeAllProperties: z.boolean().optional().describe("Whether to include all properties in the results"),
48
- includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition"),
49
- taskIdFilter: z.string().optional().describe("Task ID to filter build definitions"),
50
- processType: z.number().optional().describe("Process type to filter build definitions"),
51
- yamlFilename: z.string().optional().describe("YAML filename to filter build definitions"),
52
- }, async ({ project, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
53
- const connection = await connectionProvider();
54
- const buildApi = await connection.getBuildApi();
55
- // Auto-resolve repositoryId from name to GUID for Azure Repos
56
- let resolvedRepositoryId = repositoryId;
57
- if (repositoryId) {
58
- const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(repositoryId);
59
- if (!isGuid && (!repositoryType || repositoryType === "TfsGit")) {
60
- const gitApi = await connection.getGitApi();
61
- const repositories = await gitApi.getRepositories(project);
62
- const repo = repositories?.find((r) => r.name === repositoryId);
63
- if (!repo?.id) {
64
- return {
65
- content: [{ type: "text", text: `Error: Repository '${repositoryId}' not found in project '${project}'.` }],
66
- isError: true,
67
- };
68
- }
69
- resolvedRepositoryId = repo.id;
70
- }
71
- }
72
- const buildDefinitions = await buildApi.getDefinitions(project, name, resolvedRepositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
73
- return {
74
- content: [{ type: "text", text: JSON.stringify(buildDefinitions, null, 2) }],
75
- };
76
- });
77
- const variableSchema = z.object({
78
- value: z.string().optional(),
79
- isSecret: z.boolean().optional(),
80
- });
81
- server.tool(PIPELINE_TOOLS.pipelines_create_pipeline, "Creates a pipeline definition with YAML configuration for a given project.", {
82
- project: z.string().describe("Project ID or name to run the build in."),
83
- name: z.string().describe("Name of the new pipeline."),
84
- folder: z.string().optional().describe("Folder path for the new pipeline. Defaults to '\\' if not specified."),
85
- yamlPath: z.string().describe("The path to the pipeline's YAML file in the repository"),
86
- repositoryType: z.enum(getEnumKeys(RepositoryType)).describe("The type of repository where the pipeline's YAML file is located."),
87
- repositoryName: z.string().describe("The name of the repository. In case of GitHub repository, this is the full name (:owner/:repo) - e.g. octocat/Hello-World."),
88
- repositoryId: z.string().optional().describe("The ID of the repository."),
89
- repositoryConnectionId: z.string().optional().describe("The service connection ID for GitHub repositories. Not required for Azure Repos Git."),
90
- }, async ({ project, name, folder, yamlPath, repositoryType, repositoryName, repositoryId, repositoryConnectionId }) => {
91
- const connection = await connectionProvider();
11
+ import { dispatchAction, errorResult } from "../shared/command.js";
12
+ import { pipelinesWriteShape } from "./pipelines.dto.js";
13
+ // ─── pipelines_write commands ────────────────────────────────────────────────
14
+ // Each write action is a self-contained command. Shared infrastructure arrives
15
+ // via `CommandContext`; the action-specific input arrives via a single typed
16
+ // args object (see pipelines.dto.ts). This keeps the dispatcher agnostic of
17
+ // individual argument lists.
18
+ const runPipelineCommand = {
19
+ async execute(context, args) {
20
+ if (!args.pipelineId)
21
+ return errorResult("pipelineId is required for run_pipeline");
22
+ if (!args.previewRun && args.yamlOverride)
23
+ throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
24
+ const connection = await context.connectionProvider();
92
25
  const pipelinesApi = await connection.getPipelinesApi();
93
- const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
94
- const repositoryPayload = {
95
- type: repositoryType,
26
+ const runRequest = {
27
+ previewRun: args.previewRun,
28
+ resources: { ...args.resources },
29
+ stagesToSkip: args.stagesToSkip,
30
+ templateParameters: args.templateParameters,
31
+ variables: args.variables,
32
+ yamlOverride: args.yamlOverride,
96
33
  };
34
+ const pipelineRun = await pipelinesApi.runPipeline(runRequest, args.project, args.pipelineId, args.pipelineVersion);
35
+ if (pipelineRun.id === undefined)
36
+ throw new Error("Failed to get build ID from pipeline run");
37
+ return { content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }] };
38
+ },
39
+ };
40
+ const createPipelineCommand = {
41
+ async execute(context, args) {
42
+ if (!args.name)
43
+ return errorResult("name is required for create_pipeline");
44
+ if (!args.yamlPath)
45
+ return errorResult("yamlPath is required for create_pipeline");
46
+ if (!args.repositoryType)
47
+ return errorResult("repositoryType is required for create_pipeline");
48
+ if (!args.repositoryName)
49
+ return errorResult("repositoryName is required for create_pipeline");
50
+ const connection = await context.connectionProvider();
51
+ const pipelinesApi = await connection.getPipelinesApi();
52
+ const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, args.repositoryType);
53
+ const repositoryPayload = { type: args.repositoryType };
97
54
  if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
98
- repositoryPayload.id = repositoryId;
99
- repositoryPayload.name = repositoryName;
55
+ repositoryPayload.id = args.repositoryId;
56
+ repositoryPayload.name = args.repositoryName;
100
57
  }
101
58
  else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
102
- if (!repositoryConnectionId) {
59
+ if (!args.repositoryConnectionId)
103
60
  throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
104
- }
105
- repositoryPayload.connection = { id: repositoryConnectionId };
106
- repositoryPayload.fullname = repositoryName;
61
+ repositoryPayload.connection = { id: args.repositoryConnectionId };
62
+ repositoryPayload.fullname = args.repositoryName;
107
63
  }
108
64
  else {
109
65
  throw new Error("Unsupported repository type");
110
66
  }
111
67
  const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
112
- const createPipelineParams = {
113
- name: name,
114
- folder: folder || "\\",
115
- configuration: {
116
- type: yamlConfigurationType,
117
- path: yamlPath,
118
- repository: repositoryPayload,
119
- variables: undefined,
120
- },
121
- };
122
- const newPipeline = await pipelinesApi.createPipeline(createPipelineParams, project);
123
- return {
124
- content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }],
125
- };
126
- });
127
- server.tool(PIPELINE_TOOLS.pipelines_get_build_definition_revisions, "Retrieves a list of revisions for a specific build definition.", {
128
- project: z.string().describe("Project ID or name to get the build definition revisions for"),
129
- definitionId: z.coerce.number().min(1).describe("ID of the build definition to get revisions for"),
130
- }, async ({ project, definitionId }) => {
131
- const connection = await connectionProvider();
132
- const buildApi = await connection.getBuildApi();
133
- const revisions = await buildApi.getDefinitionRevisions(project, definitionId);
134
- return {
135
- content: [{ type: "text", text: JSON.stringify(revisions, null, 2) }],
136
- };
137
- });
138
- server.tool(PIPELINE_TOOLS.pipelines_get_builds, "Retrieves a list of builds for a given project.", {
139
- project: z.string().describe("Project ID or name to get builds for"),
140
- definitions: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter builds"),
141
- queues: z.array(z.coerce.number().min(1)).optional().describe("Array of queue IDs to filter builds"),
142
- buildNumber: z.string().optional().describe("Build number to filter builds"),
143
- minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds"),
144
- maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds"),
145
- requestedFor: z.string().optional().describe("User ID or name who requested the build"),
146
- reasonFilter: z.number().optional().describe("Reason filter for the build (see BuildReason enum)"),
147
- statusFilter: z.number().optional().describe("Status filter for the build (see BuildStatus enum)"),
148
- resultFilter: z.number().optional().describe("Result filter for the build (see BuildResult enum)"),
149
- tagFilters: z.array(z.string()).optional().describe("Array of tags to filter builds"),
150
- properties: z.array(z.string()).optional().describe("Array of property names to include in the results"),
151
- top: z.number().optional().describe("Maximum number of builds to return"),
152
- continuationToken: z.string().optional().describe("Token for continuing paged results"),
153
- maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition"),
154
- deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum)"),
155
- queryOrder: z
156
- .enum(getEnumKeys(BuildQueryOrder))
157
- .default("QueueTimeDescending")
158
- .optional()
159
- .describe("Order in which builds are returned"),
160
- branchName: z.string().optional().describe("Branch name to filter builds"),
161
- buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of build IDs to retrieve"),
162
- repositoryId: z.string().optional().describe("Repository ID to filter builds"),
163
- repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter builds"),
164
- }, async ({ project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType, }) => {
165
- const connection = await connectionProvider();
166
- const buildApi = await connection.getBuildApi();
167
- const builds = await buildApi.getBuilds(project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, safeEnumConvert(BuildQueryOrder, queryOrder), branchName, buildIds, repositoryId, repositoryType);
168
- return {
169
- content: [{ type: "text", text: JSON.stringify(builds, null, 2) }],
170
- };
171
- });
172
- server.tool(PIPELINE_TOOLS.pipelines_get_build_log, "Retrieves the logs for a specific build.", {
173
- project: z.string().describe("Project ID or name to get the build log for"),
174
- buildId: z.coerce.number().min(1).describe("ID of the build to get the log for"),
175
- }, async ({ project, buildId }) => {
176
- const connection = await connectionProvider();
177
- const buildApi = await connection.getBuildApi();
178
- const logs = await buildApi.getBuildLogs(project, buildId);
179
- return {
180
- content: [{ type: "text", text: JSON.stringify(logs, null, 2) }],
181
- };
182
- });
183
- server.tool(PIPELINE_TOOLS.pipelines_get_build_log_by_id, "Get a specific build log by log ID.", {
184
- project: z.string().describe("Project ID or name to get the build log for"),
185
- buildId: z.coerce.number().min(1).describe("ID of the build to get the log for"),
186
- logId: z.coerce.number().min(1).describe("ID of the log to retrieve"),
187
- startLine: z.coerce.number().optional().describe("Starting line number for the log content, defaults to 0"),
188
- endLine: z.coerce.number().optional().describe("Ending line number for the log content, defaults to the end of the log"),
189
- }, async ({ project, buildId, logId, startLine, endLine }) => {
190
- const connection = await connectionProvider();
191
- const buildApi = await connection.getBuildApi();
192
- const logLines = await buildApi.getBuildLogLines(project, buildId, logId, startLine, endLine);
193
- return createExternalContentResponse(logLines, "build log");
194
- });
195
- server.tool(PIPELINE_TOOLS.pipelines_get_build_changes, "Get the changes associated with a specific build.", {
196
- project: z.string().describe("Project ID or name to get the build changes for"),
197
- buildId: z.coerce.number().min(1).describe("ID of the build to get changes for"),
198
- continuationToken: z.string().optional().describe("Continuation token for pagination"),
199
- top: z.number().default(100).describe("Number of changes to retrieve, defaults to 100"),
200
- includeSourceChange: z.boolean().optional().describe("Whether to include source changes in the results, defaults to false"),
201
- }, async ({ project, buildId, continuationToken, top, includeSourceChange }) => {
202
- const connection = await connectionProvider();
203
- const buildApi = await connection.getBuildApi();
204
- const changes = await buildApi.getBuildChanges(project, buildId, continuationToken, top, includeSourceChange);
205
- return {
206
- content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
207
- };
208
- });
209
- server.tool(PIPELINE_TOOLS.pipelines_get_run, "Gets a run for a particular pipeline.", {
210
- project: z.string().describe("Project ID or name to run the build in"),
211
- pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
212
- runId: z.coerce.number().min(1).describe("ID of the run to get"),
213
- }, async ({ project, pipelineId, runId }) => {
214
- const connection = await connectionProvider();
215
- const pipelinesApi = await connection.getPipelinesApi();
216
- const pipelineRun = await pipelinesApi.getRun(project, pipelineId, runId);
217
- return {
218
- content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
219
- };
220
- });
221
- server.tool(PIPELINE_TOOLS.pipelines_list_runs, "Gets top 10000 runs for a particular pipeline.", {
222
- project: z.string().describe("Project ID or name to run the build in"),
223
- pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
224
- }, async ({ project, pipelineId }) => {
225
- const connection = await connectionProvider();
226
- const pipelinesApi = await connection.getPipelinesApi();
227
- const pipelineRuns = await pipelinesApi.listRuns(project, pipelineId);
228
- return {
229
- content: [{ type: "text", text: JSON.stringify(pipelineRuns, null, 2) }],
230
- };
231
- });
232
- const resourcesSchema = z.object({
233
- builds: z
234
- .record(z.string().describe("Name of the build resource."), z.object({
235
- version: z.string().optional().describe("Version of the build resource."),
236
- }))
237
- .optional(),
238
- containers: z
239
- .record(z.string().describe("Name of the container resource."), z.object({
240
- version: z.string().optional().describe("Version of the container resource."),
241
- }))
242
- .optional(),
243
- packages: z
244
- .record(z.string().describe("Name of the package resource."), z.object({
245
- version: z.string().optional().describe("Version of the package resource."),
246
- }))
247
- .optional(),
248
- pipelines: z.record(z.string().describe("Name of the pipeline resource."), z.object({
249
- runId: z.coerce.number().min(1).optional().describe("Id of the source pipeline run that triggered or is referenced by this pipeline run."),
250
- version: z.string().optional().describe("Version of the source pipeline run."),
251
- })),
252
- repositories: z
253
- .record(z.string().describe("Name of the repository resource."), z.object({
254
- refName: z.string().describe("Reference name, e.g., refs/heads/main."),
255
- token: z.string().optional(),
256
- tokenType: z.string().optional(),
257
- version: z.string().optional().describe("Version of the repository resource, git commit sha."),
258
- }))
259
- .optional(),
260
- });
261
- server.tool(PIPELINE_TOOLS.pipelines_run_pipeline, "Starts a new run of a pipeline.", {
262
- project: z.string().describe("Project ID or name to run the build in"),
263
- pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
264
- pipelineVersion: z.coerce.number().min(1).optional().describe("Version of the pipeline to run. If not provided, the latest version will be used."),
265
- previewRun: z.boolean().optional().describe("If true, returns the final YAML document after parsing templates without creating a new run."),
266
- resources: resourcesSchema.optional().describe("A dictionary of resources to pass to the pipeline."),
267
- stagesToSkip: z.array(z.string()).optional().describe("A list of stages to skip."),
268
- templateParameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs"),
269
- variables: z.record(z.string(), variableSchema).optional().describe("A dictionary of variables to pass to the pipeline."),
270
- yamlOverride: z.string().optional().describe("YAML override for the pipeline run."),
271
- }, async ({ project, pipelineId, pipelineVersion, previewRun, resources, stagesToSkip, templateParameters, variables, yamlOverride }) => {
272
- if (!previewRun && yamlOverride) {
273
- throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
274
- }
275
- const connection = await connectionProvider();
276
- const pipelinesApi = await connection.getPipelinesApi();
277
- const runRequest = {
278
- previewRun: previewRun,
279
- resources: {
280
- ...resources,
281
- },
282
- stagesToSkip: stagesToSkip,
283
- templateParameters: templateParameters,
284
- variables: variables,
285
- yamlOverride: yamlOverride,
286
- };
287
- const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, pipelineId, pipelineVersion);
288
- const queuedBuild = { id: pipelineRun.id };
289
- const buildId = queuedBuild.id;
290
- if (buildId === undefined) {
291
- throw new Error("Failed to get build ID from pipeline run");
292
- }
293
- return {
294
- content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
295
- };
296
- });
297
- server.tool(PIPELINE_TOOLS.pipelines_get_build_status, "Fetches the status of a specific build.", {
298
- project: z.string().describe("Project ID or name to get the build status for"),
299
- buildId: z.coerce.number().min(1).describe("ID of the build to get the status for"),
300
- }, async ({ project, buildId }) => {
301
- const connection = await connectionProvider();
302
- const buildApi = await connection.getBuildApi();
303
- const build = await buildApi.getBuildReport(project, buildId);
304
- return {
305
- content: [{ type: "text", text: JSON.stringify(build, null, 2) }],
68
+ const createParams = {
69
+ name: args.name,
70
+ folder: args.folder || "\\",
71
+ configuration: { type: yamlConfigurationType, path: args.yamlPath, repository: repositoryPayload, variables: undefined },
306
72
  };
307
- });
308
- server.tool(PIPELINE_TOOLS.pipelines_update_build_stage, "Updates the stage of a specific build.", {
309
- project: z.string().describe("Project ID or name to update the build stage for"),
310
- buildId: z.coerce.number().min(1).describe("ID of the build to update"),
311
- stageName: z.string().describe("Name of the stage to update"),
312
- status: z.enum(getEnumKeys(StageUpdateType)).describe("New status for the stage"),
313
- forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage."),
314
- }, async ({ project, buildId, stageName, status, forceRetryAllJobs }) => {
315
- const connection = await connectionProvider();
73
+ const newPipeline = await pipelinesApi.createPipeline(createParams, args.project);
74
+ return { content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }] };
75
+ },
76
+ };
77
+ const updateBuildStageCommand = {
78
+ async execute(context, args) {
79
+ if (!args.buildId)
80
+ return errorResult("buildId is required for update_build_stage");
81
+ if (!args.stageName)
82
+ return errorResult("stageName is required for update_build_stage");
83
+ if (!args.status)
84
+ return errorResult("status is required for update_build_stage");
85
+ const connection = await context.connectionProvider();
316
86
  const orgUrl = connection.serverUrl;
317
- const endpoint = `${orgUrl}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/stages/${encodeURIComponent(stageName)}?api-version=${apiVersion}`;
318
- const token = await tokenProvider();
319
- const body = {
320
- forceRetryAllJobs: forceRetryAllJobs,
321
- state: safeEnumConvert(StageUpdateType, status),
322
- };
87
+ const endpoint = `${orgUrl}/${encodeURIComponent(args.project)}/_apis/build/builds/${args.buildId}/stages/${encodeURIComponent(args.stageName)}?api-version=${apiVersion}`;
88
+ const token = await context.tokenProvider();
89
+ const body = { forceRetryAllJobs: args.forceRetryAllJobs, state: safeEnumConvert(StageUpdateType, args.status) };
323
90
  const response = await fetch(endpoint, {
324
91
  method: "PATCH",
325
- headers: {
326
- "Content-Type": "application/json",
327
- "Authorization": `Bearer ${token}`,
328
- "User-Agent": userAgentProvider(),
329
- },
92
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": context.userAgentProvider() },
330
93
  body: JSON.stringify(body),
331
94
  });
332
95
  if (!response.ok) {
@@ -334,82 +97,296 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
334
97
  throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
335
98
  }
336
99
  const updatedBuild = await response.text();
337
- return {
338
- content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }],
339
- };
100
+ return { content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }] };
101
+ },
102
+ };
103
+ /**
104
+ * The registry is the lookup table that couples each action to its command.
105
+ * Adding a new write action means registering one entry here — the dispatcher
106
+ * (`dispatchAction`) never changes.
107
+ */
108
+ const pipelinesWriteCommands = {
109
+ run_pipeline: runPipelineCommand,
110
+ create_pipeline: createPipelineCommand,
111
+ update_build_stage: updateBuildStageCommand,
112
+ };
113
+ const pipelinesWriteErrorPrefixes = {
114
+ run_pipeline: "Error running pipeline: ",
115
+ create_pipeline: "Error creating pipeline: ",
116
+ update_build_stage: "Error updating build stage: ",
117
+ };
118
+ const PIPELINE_TOOLS = {
119
+ pipelines_build: "pipelines_build",
120
+ pipelines_build_log: "pipelines_build_log",
121
+ pipelines_definition: "pipelines_definition",
122
+ pipelines_run: "pipelines_run",
123
+ pipelines_artifact: "pipelines_artifact",
124
+ pipelines_write: "pipelines_write",
125
+ };
126
+ function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
127
+ // ─── pipelines_build ────────────────────────────────────────────────────────
128
+ server.tool(PIPELINE_TOOLS.pipelines_build, "Retrieve build data for a project. Use the action parameter to specify the operation.", {
129
+ action: z
130
+ .enum(["list", "get_status", "get_changes"])
131
+ .describe("The action to perform. Options: list (list builds with optional filters), get_status (get status, issues, and report metadata for a build), get_changes (get commits and work items associated with a build)."),
132
+ project: z.string().describe("Project ID or name."),
133
+ buildId: z.coerce.number().min(1).optional().describe("ID of the build. Required for: get_status, get_changes."),
134
+ // list-specific
135
+ definitions: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter builds. Used for: list."),
136
+ queues: z.array(z.coerce.number().min(1)).optional().describe("Array of queue IDs to filter builds. Used for: list."),
137
+ buildNumber: z.string().optional().describe("Build number to filter builds. Used for: list."),
138
+ minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds. Used for: list."),
139
+ maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds. Used for: list."),
140
+ requestedFor: z.string().optional().describe("User ID or name who requested the build. Used for: list."),
141
+ reasonFilter: z.number().optional().describe("Reason filter (see BuildReason enum). Used for: list."),
142
+ statusFilter: z.number().optional().describe("Status filter (see BuildStatus enum). Used for: list."),
143
+ resultFilter: z.number().optional().describe("Result filter (see BuildResult enum). Used for: list."),
144
+ tagFilters: z.array(z.string()).optional().describe("Array of tags to filter builds. Used for: list."),
145
+ properties: z.array(z.string()).optional().describe("Array of property names to include in results. Used for: list."),
146
+ top: z.number().optional().describe("Maximum number of builds to return. Used for: list, get_changes."),
147
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list, get_changes."),
148
+ maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition. Used for: list."),
149
+ deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum). Used for: list."),
150
+ queryOrder: z.string().optional().describe("Order in which builds are returned (BuildQueryOrder values). Used for: list."),
151
+ branchName: z.string().optional().describe("Branch name to filter builds. Used for: list."),
152
+ buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of specific build IDs to retrieve. Used for: list."),
153
+ repositoryId: z.string().optional().describe("Repository ID to filter builds. Used for: list."),
154
+ repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter builds. Used for: list."),
155
+ // get_changes-specific
156
+ includeSourceChange: z.boolean().optional().describe("Whether to include source changes in results. Used for: get_changes."),
157
+ }, async ({ action, project, buildId, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType, includeSourceChange, }) => {
158
+ try {
159
+ const connection = await connectionProvider();
160
+ const buildApi = await connection.getBuildApi();
161
+ if (action === "list") {
162
+ const builds = await buildApi.getBuilds(project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, safeEnumConvert(BuildQueryOrder, queryOrder), branchName, buildIds, repositoryId, repositoryType);
163
+ return { content: [{ type: "text", text: JSON.stringify(builds, null, 2) }] };
164
+ }
165
+ if (action === "get_status") {
166
+ if (!buildId)
167
+ return { content: [{ type: "text", text: "buildId is required for get_status" }], isError: true };
168
+ const build = await buildApi.getBuildReport(project, buildId);
169
+ return { content: [{ type: "text", text: JSON.stringify(build, null, 2) }] };
170
+ }
171
+ if (action === "get_changes") {
172
+ if (!buildId)
173
+ return { content: [{ type: "text", text: "buildId is required for get_changes" }], isError: true };
174
+ const changes = await buildApi.getBuildChanges(project, buildId, continuationToken, top, includeSourceChange);
175
+ return { content: [{ type: "text", text: JSON.stringify(changes, null, 2) }] };
176
+ }
177
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
178
+ }
179
+ catch (error) {
180
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
181
+ const msgs = {
182
+ list: `Error fetching builds: ${errorMessage}`,
183
+ get_status: `Error fetching build: ${errorMessage}`,
184
+ get_changes: `Error fetching build changes: ${errorMessage}`,
185
+ };
186
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
187
+ }
340
188
  });
341
- server.tool(PIPELINE_TOOLS.pipelines_list_artifacts, "Lists artifacts for a given build.", {
342
- project: z.string().describe("The name or ID of the project."),
343
- buildId: z.coerce.number().min(1).describe("The ID of the build."),
344
- }, async ({ project, buildId }) => {
345
- const connection = await connectionProvider();
346
- const buildApi = await connection.getBuildApi();
347
- const artifacts = await buildApi.getArtifacts(project, buildId);
348
- return {
349
- content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }],
350
- };
189
+ // ─── pipelines_build_log ────────────────────────────────────────────────────
190
+ server.tool(PIPELINE_TOOLS.pipelines_build_log, "Retrieve build log data for a project. Use the action parameter to specify the operation.", {
191
+ action: z.enum(["list", "get_content"]).describe("The action to perform. Options: list (list available logs for a build), get_content (get the text content of a specific log by ID)."),
192
+ project: z.string().describe("Project ID or name."),
193
+ buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
194
+ logId: z.coerce.number().min(1).optional().describe("ID of the log to retrieve. Required for: get_content."),
195
+ startLine: z.coerce.number().optional().describe("Starting line number for the log content, defaults to 0. Used for: get_content."),
196
+ endLine: z.coerce.number().optional().describe("Ending line number for the log content, defaults to end of log. Used for: get_content."),
197
+ }, async ({ action, project, buildId, logId, startLine, endLine }) => {
198
+ try {
199
+ const connection = await connectionProvider();
200
+ const buildApi = await connection.getBuildApi();
201
+ if (action === "list") {
202
+ const logs = await buildApi.getBuildLogs(project, buildId);
203
+ return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
204
+ }
205
+ if (action === "get_content") {
206
+ if (!logId)
207
+ return { content: [{ type: "text", text: "logId is required for get_content" }], isError: true };
208
+ const logLines = await buildApi.getBuildLogLines(project, buildId, logId, startLine, endLine);
209
+ return createExternalContentResponse(logLines, "build log");
210
+ }
211
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
212
+ }
213
+ catch (error) {
214
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
215
+ const msgs = {
216
+ list: `Error fetching build log: ${errorMessage}`,
217
+ get_content: `Error fetching build log: ${errorMessage}`,
218
+ };
219
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
220
+ }
351
221
  });
352
- server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact. When destinationPath is provided, it must be a relative local path; absolute paths and path traversal are not allowed.", {
353
- project: z.string().describe("The name or ID of the project."),
354
- buildId: z.coerce.number().min(1).describe("The ID of the build."),
355
- artifactName: z.string().describe("The name of the artifact to download."),
356
- destinationPath: z.string().optional().describe("The relative local path to download the artifact to. If not provided, returns binary content as base64."),
357
- }, async ({ project, buildId, artifactName, destinationPath }) => {
358
- const hasUnsafePathSegment = (value) => value.split(/[\\/]+/).some((segment) => segment === "." || segment === "..");
359
- const hasPathSeparators = (value) => /[\\/]/.test(value);
360
- const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
361
- const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
362
- if (hasUnsafePathSegment(artifactName) || hasPathSeparators(artifactName) || hasDriveLetter(artifactName) || isAbsolutePath(artifactName)) {
363
- throw new Error("Invalid artifactName: artifactName must be a file name, not a path.");
222
+ // ─── pipelines_definition ───────────────────────────────────────────────────
223
+ server.tool(PIPELINE_TOOLS.pipelines_definition, "Retrieve pipeline definition data for a project. Use the action parameter to specify the operation.", {
224
+ action: z
225
+ .enum(["list", "list_revisions"])
226
+ .describe("The action to perform. Options: list (list pipeline definitions with optional filters), list_revisions (list revision history for a pipeline definition)."),
227
+ project: z.string().describe("Project ID or name."),
228
+ definitionId: z.coerce.number().min(1).optional().describe("ID of the build definition. Required for: list_revisions."),
229
+ // list-specific
230
+ repositoryId: z.string().optional().describe("Repository ID to filter definitions. Can be a GUID or name (auto-resolved for TfsGit). Used for: list."),
231
+ repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter definitions. Used for: list."),
232
+ name: z.string().optional().describe("Name filter for build definitions. Used for: list."),
233
+ path: z.string().optional().describe("Path filter for build definitions. Used for: list."),
234
+ queryOrder: z.string().optional().describe("Order in which definitions are returned (DefinitionQueryOrder values). Used for: list."),
235
+ top: z.number().optional().describe("Maximum number of definitions to return. Used for: list."),
236
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list."),
237
+ minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter definitions. Used for: list."),
238
+ definitionIds: z.array(z.coerce.number().min(1)).optional().describe("Array of definition IDs to filter. Used for: list."),
239
+ builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date. Used for: list."),
240
+ notBuiltAfter: z.coerce.date().optional().describe("Return definitions without builds after this date. Used for: list."),
241
+ includeAllProperties: z.boolean().optional().describe("Whether to include all properties in results. Used for: list."),
242
+ includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition. Used for: list."),
243
+ taskIdFilter: z.string().optional().describe("Task ID to filter build definitions. Used for: list."),
244
+ processType: z.number().optional().describe("Process type to filter build definitions. Used for: list."),
245
+ yamlFilename: z.string().optional().describe("YAML filename to filter build definitions. Used for: list."),
246
+ }, async ({ action, project, definitionId, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
247
+ try {
248
+ const connection = await connectionProvider();
249
+ const buildApi = await connection.getBuildApi();
250
+ if (action === "list") {
251
+ let resolvedRepositoryId = repositoryId;
252
+ if (repositoryId) {
253
+ const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(repositoryId);
254
+ if (!isGuid && (!repositoryType || repositoryType === "TfsGit")) {
255
+ const gitApi = await connection.getGitApi();
256
+ const repositories = await gitApi.getRepositories(project);
257
+ const repo = repositories?.find((r) => r.name === repositoryId);
258
+ if (!repo?.id) {
259
+ return { content: [{ type: "text", text: `Error: Repository '${repositoryId}' not found in project '${project}'.` }], isError: true };
260
+ }
261
+ resolvedRepositoryId = repo.id;
262
+ }
263
+ }
264
+ const defs = await buildApi.getDefinitions(project, name, resolvedRepositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
265
+ return { content: [{ type: "text", text: JSON.stringify(defs, null, 2) }] };
266
+ }
267
+ if (action === "list_revisions") {
268
+ if (!definitionId)
269
+ return { content: [{ type: "text", text: "definitionId is required for list_revisions" }], isError: true };
270
+ const revisions = await buildApi.getDefinitionRevisions(project, definitionId);
271
+ return { content: [{ type: "text", text: JSON.stringify(revisions, null, 2) }] };
272
+ }
273
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
364
274
  }
365
- if (destinationPath && (hasUnsafePathSegment(destinationPath) || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
366
- throw new Error("Invalid destinationPath: use a relative path without path traversal.");
275
+ catch (error) {
276
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
277
+ const msgs = {
278
+ list: `Error fetching build definitions: ${errorMessage}`,
279
+ list_revisions: `Error fetching build definition revisions: ${errorMessage}`,
280
+ };
281
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
367
282
  }
368
- const connection = await connectionProvider();
369
- const buildApi = await connection.getBuildApi();
370
- const artifact = await buildApi.getArtifact(project, buildId, artifactName);
371
- if (!artifact) {
372
- return {
373
- content: [{ type: "text", text: `Artifact ${artifactName} not found in build ${buildId}.` }],
283
+ });
284
+ // ─── pipelines_run ──────────────────────────────────────────────────────────
285
+ server.tool(PIPELINE_TOOLS.pipelines_run, "Retrieve pipeline run data for a project. Use the action parameter to specify the operation.", {
286
+ action: z.enum(["get", "list"]).describe("The action to perform. Options: get (get a single pipeline run), list (list runs for a pipeline)."),
287
+ project: z.string().describe("Project ID or name."),
288
+ pipelineId: z.coerce.number().min(1).describe("ID of the pipeline. Required for all actions."),
289
+ runId: z.coerce.number().min(1).optional().describe("ID of the run. Required for: get."),
290
+ }, async ({ action, project, pipelineId, runId }) => {
291
+ try {
292
+ const connection = await connectionProvider();
293
+ const pipelinesApi = await connection.getPipelinesApi();
294
+ if (action === "get") {
295
+ if (!runId)
296
+ return { content: [{ type: "text", text: "runId is required for get" }], isError: true };
297
+ const run = await pipelinesApi.getRun(project, pipelineId, runId);
298
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
299
+ }
300
+ if (action === "list") {
301
+ const runs = await pipelinesApi.listRuns(project, pipelineId);
302
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
303
+ }
304
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
305
+ }
306
+ catch (error) {
307
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
308
+ const msgs = {
309
+ get: `Error fetching pipeline run: ${errorMessage}`,
310
+ list: `Error fetching pipeline runs: ${errorMessage}`,
374
311
  };
312
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
375
313
  }
376
- const fileStream = await buildApi.getArtifactContentZip(project, buildId, artifactName);
377
- // If destinationPath is provided, save to disk
378
- if (destinationPath) {
379
- const fullDestinationPath = resolve(destinationPath);
380
- mkdirSync(fullDestinationPath, { recursive: true });
381
- const fileDestinationPath = join(fullDestinationPath, `${artifactName}.zip`);
382
- const writeStream = createWriteStream(fileDestinationPath);
383
- await new Promise((resolve, reject) => {
384
- fileStream.pipe(writeStream);
385
- fileStream.on("end", () => resolve());
386
- fileStream.on("error", (err) => reject(err));
387
- });
388
- return {
389
- content: [{ type: "text", text: `Artifact ${artifactName} downloaded to ${destinationPath}.` }],
314
+ });
315
+ server.tool(PIPELINE_TOOLS.pipelines_artifact, "Retrieve and download build artifacts. Use the action parameter to specify the operation.", {
316
+ action: z.enum(["list", "download"]).describe("The action to perform. Options: list (list artifacts for a build), download (download a named build artifact)."),
317
+ project: z.string().describe("Project ID or name."),
318
+ buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
319
+ artifactName: z.string().optional().describe("Name of the artifact. Required for: download."),
320
+ destinationPath: z.string().optional().describe("Relative local path to download the artifact to. If not provided, returns base64 content. Used for: download."),
321
+ }, async ({ action, project, buildId, artifactName, destinationPath }) => {
322
+ try {
323
+ // Validate artifact/path inputs before making any network calls
324
+ if (action === "download") {
325
+ if (!artifactName)
326
+ return { content: [{ type: "text", text: "artifactName is required for download" }], isError: true };
327
+ const hasUnsafePathSegment = (value) => value.split(/[\\/]+/).some((segment) => segment === "." || segment === "..");
328
+ const hasPathSeparators = (value) => /[\\/]/.test(value);
329
+ const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
330
+ const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
331
+ if (hasUnsafePathSegment(artifactName) || hasPathSeparators(artifactName) || hasDriveLetter(artifactName) || isAbsolutePath(artifactName)) {
332
+ throw new Error("Invalid artifactName: artifactName must be a file name, not a path.");
333
+ }
334
+ if (destinationPath && (hasUnsafePathSegment(destinationPath) || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
335
+ throw new Error("Invalid destinationPath: use a relative path without path traversal.");
336
+ }
337
+ }
338
+ const connection = await connectionProvider();
339
+ const buildApi = await connection.getBuildApi();
340
+ if (action === "list") {
341
+ const artifacts = await buildApi.getArtifacts(project, buildId);
342
+ return { content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }] };
343
+ }
344
+ if (action === "download") {
345
+ const resolvedArtifactName = artifactName; // validated in pre-flight check above
346
+ const artifact = await buildApi.getArtifact(project, buildId, resolvedArtifactName);
347
+ if (!artifact) {
348
+ return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} not found in build ${buildId}.` }], isError: true };
349
+ }
350
+ const fileStream = await buildApi.getArtifactContentZip(project, buildId, resolvedArtifactName);
351
+ if (destinationPath) {
352
+ const fullDestinationPath = resolve(destinationPath);
353
+ mkdirSync(fullDestinationPath, { recursive: true });
354
+ const fileDestinationPath = join(fullDestinationPath, `${resolvedArtifactName}.zip`);
355
+ const writeStream = createWriteStream(fileDestinationPath);
356
+ await new Promise((resolve, reject) => {
357
+ fileStream.pipe(writeStream);
358
+ fileStream.on("end", () => resolve());
359
+ fileStream.on("error", (err) => reject(err));
360
+ });
361
+ return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} downloaded to ${destinationPath}.` }] };
362
+ }
363
+ const chunks = [];
364
+ await new Promise((resolve, reject) => {
365
+ fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
366
+ fileStream.on("end", () => resolve());
367
+ fileStream.on("error", (err) => reject(err));
368
+ });
369
+ const buffer = Buffer.concat(chunks);
370
+ const base64Data = buffer.toString("base64");
371
+ return {
372
+ content: [{ type: "resource", resource: { uri: `data:application/zip;base64,${base64Data}`, mimeType: "application/zip", text: base64Data } }],
373
+ };
374
+ }
375
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
376
+ }
377
+ catch (error) {
378
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
379
+ const msgs = {
380
+ list: `Error fetching artifacts: ${errorMessage}`,
381
+ download: `Error downloading artifact: ${errorMessage}`,
390
382
  };
383
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
391
384
  }
392
- // Otherwise, return binary content as base64
393
- const chunks = [];
394
- await new Promise((resolve, reject) => {
395
- fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
396
- fileStream.on("end", () => resolve());
397
- fileStream.on("error", (err) => reject(err));
398
- });
399
- const buffer = Buffer.concat(chunks);
400
- const base64Data = buffer.toString("base64");
401
- return {
402
- content: [
403
- {
404
- type: "resource",
405
- resource: {
406
- uri: `data:application/zip;base64,${base64Data}`,
407
- mimeType: "application/zip",
408
- text: base64Data,
409
- },
410
- },
411
- ],
412
- };
385
+ });
386
+ // ─── pipelines_write ────────────────────────────────────────────────────────
387
+ server.tool(PIPELINE_TOOLS.pipelines_write, "Write operations for pipelines and builds. Use the action parameter to specify the operation.", pipelinesWriteShape, async (args) => {
388
+ const context = { connectionProvider, tokenProvider, userAgentProvider };
389
+ return dispatchAction(pipelinesWriteCommands, context, args, pipelinesWriteErrorPrefixes);
413
390
  });
414
391
  }
415
- export { PIPELINE_TOOLS, configurePipelineTools };
392
+ export { PIPELINE_TOOLS, configurePipelineTools, runPipelineCommand, createPipelineCommand, updateBuildStageCommand };