@azure-devops/mcp 2.9.0-nightly.20260818 → 2.9.0-nightly.20260820

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.
@@ -56,7 +56,7 @@ export const runPipelineShape = {
56
56
  /** create_pipeline inputs. */
57
57
  export const createPipelineShape = {
58
58
  ...projectShape,
59
- name: z.string().optional().describe("Name of the new pipeline. Required for: create_pipeline."),
59
+ name: z.string().optional().describe("Pipeline name. Required for: create_pipeline, rename_pipeline."),
60
60
  folder: z.string().optional().describe("Folder path for the new pipeline. Used for: create_pipeline."),
61
61
  yamlPath: z.string().optional().describe("Path to the YAML file in the repository. Required for: create_pipeline."),
62
62
  repositoryType: z
@@ -67,6 +67,12 @@ export const createPipelineShape = {
67
67
  repositoryId: z.string().optional().describe("ID of the repository. Used for: create_pipeline."),
68
68
  repositoryConnectionId: z.string().optional().describe("Service connection ID for GitHub repositories. Used for: create_pipeline."),
69
69
  };
70
+ /** rename_pipeline inputs. */
71
+ export const renamePipelineShape = {
72
+ ...projectShape,
73
+ pipelineId: z.coerce.number().min(1).optional().describe("ID of the pipeline. Required for: run_pipeline, rename_pipeline."),
74
+ name: z.string().optional().describe("Pipeline name. Required for: create_pipeline, rename_pipeline."),
75
+ };
70
76
  /** update_build_stage inputs. */
71
77
  export const updateBuildStageShape = {
72
78
  ...projectShape,
@@ -81,15 +87,17 @@ export const updateBuildStageShape = {
81
87
  /** The composed input shape for the grouped `pipelines_write` tool. */
82
88
  export const pipelinesWriteShape = {
83
89
  action: z
84
- .enum(["run_pipeline", "create_pipeline", "update_build_stage"])
85
- .describe("The action to perform. Options: run_pipeline (queue a new pipeline run), create_pipeline (create a new YAML pipeline definition), update_build_stage (cancel, retry, or run a stage on an in-flight build)."),
90
+ .enum(["run_pipeline", "create_pipeline", "rename_pipeline", "update_build_stage"])
91
+ .describe("The action to perform. Options: run_pipeline (queue a new pipeline run), create_pipeline (create a new YAML pipeline definition), rename_pipeline (rename an existing pipeline definition), update_build_stage (cancel, retry, or run a stage on an in-flight build)."),
86
92
  ...runPipelineShape,
87
93
  ...createPipelineShape,
94
+ ...renamePipelineShape,
88
95
  ...updateBuildStageShape,
89
96
  };
90
97
  // Per-action schemas + inferred argument DTOs. `z.infer` keeps these types in
91
98
  // lockstep with the schemas above.
92
99
  export const runPipelineSchema = z.object(runPipelineShape);
93
100
  export const createPipelineSchema = z.object(createPipelineShape);
101
+ export const renamePipelineSchema = z.object(renamePipelineShape);
94
102
  export const updateBuildStageSchema = z.object(updateBuildStageShape);
95
103
  export const pipelinesWriteSchema = z.object(pipelinesWriteShape);
@@ -8,111 +8,102 @@ 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
- import { dispatchAction, errorResult } from "../shared/command.js";
12
11
  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();
25
- const pipelinesApi = await connection.getPipelinesApi();
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,
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 };
54
- if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
55
- repositoryPayload.id = args.repositoryId;
56
- repositoryPayload.name = args.repositoryName;
57
- }
58
- else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
59
- if (!args.repositoryConnectionId)
60
- throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
61
- repositoryPayload.connection = { id: args.repositoryConnectionId };
62
- repositoryPayload.fullname = args.repositoryName;
63
- }
64
- else {
65
- throw new Error("Unsupported repository type");
66
- }
67
- const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
68
- const createParams = {
69
- name: args.name,
70
- folder: args.folder || "\\",
71
- configuration: { type: yamlConfigurationType, path: args.yamlPath, repository: repositoryPayload, variables: undefined },
72
- };
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();
86
- const orgUrl = connection.serverUrl;
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) };
90
- const response = await fetch(endpoint, {
91
- method: "PATCH",
92
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": context.userAgentProvider() },
93
- body: JSON.stringify(body),
94
- });
95
- if (!response.ok) {
96
- const errorText = await response.text();
97
- throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
98
- }
99
- const updatedBuild = await response.text();
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
- };
12
+ const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
13
+ async function runPipeline(args, connectionProvider) {
14
+ if (!args.pipelineId)
15
+ return errorResult("pipelineId is required for run_pipeline");
16
+ if (!args.previewRun && args.yamlOverride)
17
+ throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
18
+ const connection = await connectionProvider();
19
+ const pipelinesApi = await connection.getPipelinesApi();
20
+ const runRequest = {
21
+ previewRun: args.previewRun,
22
+ resources: { ...args.resources },
23
+ stagesToSkip: args.stagesToSkip,
24
+ templateParameters: args.templateParameters,
25
+ variables: args.variables,
26
+ yamlOverride: args.yamlOverride,
27
+ };
28
+ const pipelineRun = await pipelinesApi.runPipeline(runRequest, args.project, args.pipelineId, args.pipelineVersion);
29
+ if (pipelineRun.id === undefined)
30
+ throw new Error("Failed to get build ID from pipeline run");
31
+ return { content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }] };
32
+ }
33
+ async function createPipeline(args, connectionProvider) {
34
+ if (!args.name)
35
+ return errorResult("name is required for create_pipeline");
36
+ if (!args.yamlPath)
37
+ return errorResult("yamlPath is required for create_pipeline");
38
+ if (!args.repositoryType)
39
+ return errorResult("repositoryType is required for create_pipeline");
40
+ if (!args.repositoryName)
41
+ return errorResult("repositoryName is required for create_pipeline");
42
+ const connection = await connectionProvider();
43
+ const pipelinesApi = await connection.getPipelinesApi();
44
+ const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, args.repositoryType);
45
+ const repositoryPayload = { type: args.repositoryType };
46
+ if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
47
+ repositoryPayload.id = args.repositoryId;
48
+ repositoryPayload.name = args.repositoryName;
49
+ }
50
+ else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
51
+ if (!args.repositoryConnectionId)
52
+ throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
53
+ repositoryPayload.connection = { id: args.repositoryConnectionId };
54
+ repositoryPayload.fullname = args.repositoryName;
55
+ }
56
+ else {
57
+ throw new Error("Unsupported repository type");
58
+ }
59
+ const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
60
+ const createParams = {
61
+ name: args.name,
62
+ folder: args.folder || "\\",
63
+ configuration: { type: yamlConfigurationType, path: args.yamlPath, repository: repositoryPayload, variables: undefined },
64
+ };
65
+ const newPipeline = await pipelinesApi.createPipeline(createParams, args.project);
66
+ return { content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }] };
67
+ }
68
+ async function renamePipeline(args, connectionProvider) {
69
+ if (!args.pipelineId)
70
+ return errorResult("pipelineId is required for rename_pipeline");
71
+ if (!args.name)
72
+ return errorResult("name is required for rename_pipeline");
73
+ const connection = await connectionProvider();
74
+ const buildApi = await connection.getBuildApi();
75
+ const definition = await buildApi.getDefinition(args.project, args.pipelineId);
76
+ const updatedDefinition = await buildApi.updateDefinition({ ...definition, name: args.name }, args.project, args.pipelineId);
77
+ return { content: [{ type: "text", text: JSON.stringify(updatedDefinition, null, 2) }] };
78
+ }
79
+ async function updateBuildStage(args, connectionProvider, tokenProvider, userAgentProvider) {
80
+ if (!args.buildId)
81
+ return errorResult("buildId is required for update_build_stage");
82
+ if (!args.stageName)
83
+ return errorResult("stageName is required for update_build_stage");
84
+ if (!args.status)
85
+ return errorResult("status is required for update_build_stage");
86
+ const connection = await connectionProvider();
87
+ const orgUrl = connection.serverUrl;
88
+ const endpoint = `${orgUrl}/${encodeURIComponent(args.project)}/_apis/build/builds/${args.buildId}/stages/${encodeURIComponent(args.stageName)}?api-version=${apiVersion}`;
89
+ const token = await tokenProvider();
90
+ const body = { forceRetryAllJobs: args.forceRetryAllJobs, state: safeEnumConvert(StageUpdateType, args.status) };
91
+ const response = await fetch(endpoint, {
92
+ method: "PATCH",
93
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": userAgentProvider() },
94
+ body: JSON.stringify(body),
95
+ });
96
+ if (!response.ok) {
97
+ const errorText = await response.text();
98
+ throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
99
+ }
100
+ const updatedBuild = await response.text();
101
+ return { content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }] };
102
+ }
113
103
  const pipelinesWriteErrorPrefixes = {
114
104
  run_pipeline: "Error running pipeline: ",
115
105
  create_pipeline: "Error creating pipeline: ",
106
+ rename_pipeline: "Error renaming pipeline: ",
116
107
  update_build_stage: "Error updating build stage: ",
117
108
  };
118
109
  const PIPELINE_TOOLS = {
@@ -385,8 +376,26 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
385
376
  });
386
377
  // ─── pipelines_write ────────────────────────────────────────────────────────
387
378
  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);
379
+ try {
380
+ switch (args.action) {
381
+ case "run_pipeline":
382
+ return await runPipeline(args, connectionProvider);
383
+ case "create_pipeline":
384
+ return await createPipeline(args, connectionProvider);
385
+ case "rename_pipeline":
386
+ return await renamePipeline(args, connectionProvider);
387
+ case "update_build_stage":
388
+ return await updateBuildStage(args, connectionProvider, tokenProvider, userAgentProvider);
389
+ default: {
390
+ const unsupportedAction = args.action;
391
+ return errorResult(`Unknown action: ${unsupportedAction}. Supported actions: ${Object.keys(pipelinesWriteErrorPrefixes).sort().join(", ")}`);
392
+ }
393
+ }
394
+ }
395
+ catch (error) {
396
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
397
+ return errorResult(`${pipelinesWriteErrorPrefixes[args.action]}${message}`);
398
+ }
390
399
  });
391
400
  }
392
- export { PIPELINE_TOOLS, configurePipelineTools, runPipelineCommand, createPipelineCommand, updateBuildStageCommand };
401
+ export { PIPELINE_TOOLS, configurePipelineTools, runPipeline, createPipeline, renamePipeline, updateBuildStage };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.9.0-nightly.20260818";
1
+ export const packageVersion = "2.9.0-nightly.20260820";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.9.0-nightly.20260818",
3
+ "version": "2.9.0-nightly.20260820",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -1,34 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- /** Builds an error `CallToolResult`. */
4
- export const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
5
- /**
6
- * Routes a validated, action-carrying args object to the matching command.
7
- *
8
- * This is what removes long positional parameter lists from grouped ("action")
9
- * tools: instead of destructuring every possible field, the whole typed args
10
- * object is forwarded to the single command keyed by `args.action`, coupling
11
- * each action to exactly one command.
12
- *
13
- * - Unknown actions short-circuit with an "Unknown action" error and never
14
- * touch the context (so no connection is opened).
15
- * - Errors thrown by a command are caught and formatted using the optional
16
- * per-action `errorPrefixes` map (falling back to a generic message).
17
- * - Errors returned by a command (e.g. validation `errorResult`s) pass through
18
- * unchanged.
19
- */
20
- export async function dispatchAction(commands, context, args, errorPrefixes) {
21
- const command = commands[args.action];
22
- if (!command) {
23
- const supportedActions = Object.keys(commands).sort().join(", ");
24
- return errorResult(`Unknown action: ${args.action}. Supported actions: ${supportedActions}`);
25
- }
26
- try {
27
- return await command.execute(context, args);
28
- }
29
- catch (error) {
30
- const message = error instanceof Error ? error.message : "Unknown error occurred";
31
- const prefix = errorPrefixes?.[args.action];
32
- return errorResult(prefix ? `${prefix}${message}` : `Error: ${message}`);
33
- }
34
- }