@azure-devops/mcp 2.8.1-nightly.20260715 → 2.8.1-nightly.20260716
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.
- package/dist/tools/advanced-security.js +31 -11
- package/dist/tools/pipelines.js +387 -376
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ const ADVSEC_TOOLS = {
|
|
|
8
8
|
get_alert_details: "advsec_get_alert_details",
|
|
9
9
|
};
|
|
10
10
|
function configureAdvSecTools(server, _, connectionProvider) {
|
|
11
|
-
server.tool(ADVSEC_TOOLS.get_alerts, "Retrieve Advanced Security alerts for a repository.", {
|
|
11
|
+
server.tool(ADVSEC_TOOLS.get_alerts, "Retrieve Advanced Security alerts for a repository. Results are scoped to the specified project and repository. Branch filters (onlyDefaultBranch, ref) apply only to code, dependency, and license alerts; they are not applicable to secret alerts and are ignored by the service, so they neither include nor exclude secrets. To narrow secret alerts by confidence, pass a single 'confidenceLevels' value ('High' or 'Other'); selecting every level is treated as no confidence filter.", {
|
|
12
12
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
13
13
|
repository: z.string().describe("The name or ID of the repository to get alerts for."),
|
|
14
14
|
alertType: z
|
|
@@ -26,17 +26,22 @@ function configureAdvSecTools(server, _, connectionProvider) {
|
|
|
26
26
|
ruleId: z.string().optional().describe("Filter alerts by rule ID."),
|
|
27
27
|
ruleName: z.string().optional().describe("Filter alerts by rule name."),
|
|
28
28
|
toolName: z.string().optional().describe("Filter alerts by tool name."),
|
|
29
|
-
ref: z
|
|
30
|
-
|
|
29
|
+
ref: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Filter non-secret alerts by git reference (branch), e.g. 'refs/heads/main'. When omitted and onlyDefaultBranch is true, only alerts on the default branch are returned. Not applicable to secret alerts and ignored by this tool when alertType is 'Secret'. When alertType is unspecified, this filter is still sent and may exclude secret alerts from the results; query alertType 'Secret' separately to retrieve all secrets."),
|
|
33
|
+
onlyDefaultBranch: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("For non-secret alerts: if true (the service default when omitted) only return alerts found on the default branch; if false, return alerts from all branches. Ignored when 'ref' is provided. Not applicable to secret alerts and ignored by this tool when alertType is 'Secret'. When alertType is unspecified, this filter is still sent and may exclude secret alerts from the results; query alertType 'Secret' separately to retrieve all secrets."),
|
|
31
37
|
confidenceLevels: z
|
|
32
38
|
.array(z.enum(getEnumKeys(Confidence)))
|
|
33
39
|
.optional()
|
|
34
|
-
.
|
|
35
|
-
.describe("Filter alerts by confidence levels. Only applicable for secret alerts. Defaults to both 'high' and 'other'."),
|
|
40
|
+
.describe("Only applicable to secret alerts. Accepted values are 'High' and 'Other'. Pass a single value (e.g. ['High']) to narrow secrets to that confidence level. Leave unset to return secrets without a confidence filter. Do not select both levels to widen results: the Alerts service does not accept a multi-value confidence filter and would return no alerts, so this tool treats an all-levels selection as no filter and omits it."),
|
|
36
41
|
validity: z
|
|
37
42
|
.array(z.enum(getEnumKeys(AlertValidityStatus)))
|
|
38
43
|
.optional()
|
|
39
|
-
.describe("
|
|
44
|
+
.describe("Only applicable to secret alerts. If omitted, alerts of all validity statuses are returned (no validity filter is applied). Filtering by validity may return fewer alerts than 'top'; use the continuation token to fetch any remaining alerts."),
|
|
40
45
|
top: z.coerce.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
|
|
41
46
|
orderBy: z.enum(["id", "firstSeen", "lastSeen", "fixedOn", "severity"]).optional().default("severity").describe("Order results by specified field. Defaults to 'severity'."),
|
|
42
47
|
continuationToken: z.string().optional().describe("Continuation token for pagination."),
|
|
@@ -44,7 +49,22 @@ function configureAdvSecTools(server, _, connectionProvider) {
|
|
|
44
49
|
try {
|
|
45
50
|
const connection = await connectionProvider();
|
|
46
51
|
const alertApi = await connection.getAlertApi();
|
|
47
|
-
const
|
|
52
|
+
const normalizedAlertType = alertType?.toLowerCase();
|
|
53
|
+
// "onlyDefaultBranch" and "ref" are not applicable to secret alerts (secrets are not
|
|
54
|
+
// branch-scoped and carry a null gitRef). Forwarding them for a secret-only query diverges
|
|
55
|
+
// from the REST API / Advanced Security UI and can incorrectly return no alerts, so only
|
|
56
|
+
// include them when the query is not restricted to secret alerts.
|
|
57
|
+
const isSecretOnly = normalizedAlertType === "secret";
|
|
58
|
+
// "confidenceLevels" and "validity" only apply to secret alerts, so include them whenever
|
|
59
|
+
// the result set can contain secrets (an explicit "secret" type or no type filter at all).
|
|
60
|
+
const canIncludeSecrets = !alertType || isSecretOnly;
|
|
61
|
+
// The Alerts service does not accept the multi-value (comma-serialized) confidence filter
|
|
62
|
+
// that the SDK emits: selecting every level (e.g. both "High" and "Other") returns zero
|
|
63
|
+
// alerts, and it is a no-op filter regardless. Only forward confidenceLevels when it
|
|
64
|
+
// narrows the result to a proper subset (a single level); otherwise omit it so secrets are
|
|
65
|
+
// returned without a confidence filter instead of an empty set.
|
|
66
|
+
const confidenceLevelValues = confidenceLevels ? mapStringArrayToEnum(confidenceLevels, Confidence) : [];
|
|
67
|
+
const narrowsByConfidence = confidenceLevelValues.length > 0 && confidenceLevelValues.length < getEnumKeys(Confidence).length;
|
|
48
68
|
const criteria = {
|
|
49
69
|
...(alertType && { alertType: mapStringToEnum(alertType, AlertType) }),
|
|
50
70
|
...(states && { states: mapStringArrayToEnum(states, State) }),
|
|
@@ -52,10 +72,10 @@ function configureAdvSecTools(server, _, connectionProvider) {
|
|
|
52
72
|
...(ruleId && { ruleId }),
|
|
53
73
|
...(ruleName && { ruleName }),
|
|
54
74
|
...(toolName && { toolName }),
|
|
55
|
-
...(ref && { ref }),
|
|
56
|
-
...(onlyDefaultBranch !== undefined && { onlyDefaultBranch }),
|
|
57
|
-
...(
|
|
58
|
-
...(
|
|
75
|
+
...(!isSecretOnly && ref && { ref }),
|
|
76
|
+
...(!isSecretOnly && onlyDefaultBranch !== undefined && { onlyDefaultBranch }),
|
|
77
|
+
...(canIncludeSecrets && narrowsByConfidence && { confidenceLevels: confidenceLevelValues }),
|
|
78
|
+
...(canIncludeSecrets && validity && { validity: mapStringArrayToEnum(validity, AlertValidityStatus) }),
|
|
59
79
|
};
|
|
60
80
|
const result = await alertApi.getAlerts(project, repository, top, orderBy, criteria, undefined, // expand parameter
|
|
61
81
|
continuationToken);
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -9,407 +9,418 @@ import { mkdirSync, createWriteStream } from "fs";
|
|
|
9
9
|
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
10
10
|
import { join, posix, resolve, win32 } from "path";
|
|
11
11
|
const PIPELINE_TOOLS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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",
|
|
12
|
+
pipelines_build: "pipelines_build",
|
|
13
|
+
pipelines_build_log: "pipelines_build_log",
|
|
14
|
+
pipelines_definition: "pipelines_definition",
|
|
15
|
+
pipelines_run: "pipelines_run",
|
|
16
|
+
pipelines_artifact: "pipelines_artifact",
|
|
17
|
+
pipelines_write: "pipelines_write",
|
|
26
18
|
};
|
|
27
19
|
function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
.optional()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
20
|
+
// ─── pipelines_build ────────────────────────────────────────────────────────
|
|
21
|
+
server.tool(PIPELINE_TOOLS.pipelines_build, "Retrieve build data for a project. Use the action parameter to specify the operation.", {
|
|
22
|
+
action: z
|
|
23
|
+
.enum(["list", "get_status", "get_changes"])
|
|
24
|
+
.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)."),
|
|
25
|
+
project: z.string().describe("Project ID or name."),
|
|
26
|
+
buildId: z.coerce.number().min(1).optional().describe("ID of the build. Required for: get_status, get_changes."),
|
|
27
|
+
// list-specific
|
|
28
|
+
definitions: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter builds. Used for: list."),
|
|
29
|
+
queues: z.array(z.coerce.number().min(1)).optional().describe("Array of queue IDs to filter builds. Used for: list."),
|
|
30
|
+
buildNumber: z.string().optional().describe("Build number to filter builds. Used for: list."),
|
|
31
|
+
minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds. Used for: list."),
|
|
32
|
+
maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds. Used for: list."),
|
|
33
|
+
requestedFor: z.string().optional().describe("User ID or name who requested the build. Used for: list."),
|
|
34
|
+
reasonFilter: z.number().optional().describe("Reason filter (see BuildReason enum). Used for: list."),
|
|
35
|
+
statusFilter: z.number().optional().describe("Status filter (see BuildStatus enum). Used for: list."),
|
|
36
|
+
resultFilter: z.number().optional().describe("Result filter (see BuildResult enum). Used for: list."),
|
|
37
|
+
tagFilters: z.array(z.string()).optional().describe("Array of tags to filter builds. Used for: list."),
|
|
38
|
+
properties: z.array(z.string()).optional().describe("Array of property names to include in results. Used for: list."),
|
|
39
|
+
top: z.number().optional().describe("Maximum number of builds to return. Used for: list, get_changes."),
|
|
40
|
+
continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list, get_changes."),
|
|
41
|
+
maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition. Used for: list."),
|
|
42
|
+
deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum). Used for: list."),
|
|
43
|
+
queryOrder: z.string().optional().describe("Order in which builds are returned (BuildQueryOrder values). Used for: list."),
|
|
44
|
+
branchName: z.string().optional().describe("Branch name to filter builds. Used for: list."),
|
|
45
|
+
buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of specific build IDs to retrieve. Used for: list."),
|
|
46
|
+
repositoryId: z.string().optional().describe("Repository ID to filter builds. Used for: list."),
|
|
47
|
+
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter builds. Used for: list."),
|
|
48
|
+
// get_changes-specific
|
|
49
|
+
includeSourceChange: z.boolean().optional().describe("Whether to include source changes in results. Used for: get_changes."),
|
|
50
|
+
}, 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, }) => {
|
|
51
|
+
try {
|
|
52
|
+
const connection = await connectionProvider();
|
|
53
|
+
const buildApi = await connection.getBuildApi();
|
|
54
|
+
if (action === "list") {
|
|
55
|
+
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);
|
|
56
|
+
return { content: [{ type: "text", text: JSON.stringify(builds, null, 2) }] };
|
|
57
|
+
}
|
|
58
|
+
if (action === "get_status") {
|
|
59
|
+
if (!buildId)
|
|
60
|
+
return { content: [{ type: "text", text: "buildId is required for get_status" }], isError: true };
|
|
61
|
+
const build = await buildApi.getBuildReport(project, buildId);
|
|
62
|
+
return { content: [{ type: "text", text: JSON.stringify(build, null, 2) }] };
|
|
63
|
+
}
|
|
64
|
+
if (action === "get_changes") {
|
|
65
|
+
if (!buildId)
|
|
66
|
+
return { content: [{ type: "text", text: "buildId is required for get_changes" }], isError: true };
|
|
67
|
+
const changes = await buildApi.getBuildChanges(project, buildId, continuationToken, top, includeSourceChange);
|
|
68
|
+
return { content: [{ type: "text", text: JSON.stringify(changes, null, 2) }] };
|
|
70
69
|
}
|
|
70
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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();
|
|
92
|
-
const pipelinesApi = await connection.getPipelinesApi();
|
|
93
|
-
const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
|
|
94
|
-
const repositoryPayload = {
|
|
95
|
-
type: repositoryType,
|
|
96
|
-
};
|
|
97
|
-
if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
|
|
98
|
-
repositoryPayload.id = repositoryId;
|
|
99
|
-
repositoryPayload.name = repositoryName;
|
|
72
|
+
catch (error) {
|
|
73
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
74
|
+
const msgs = {
|
|
75
|
+
list: `Error fetching builds: ${errorMessage}`,
|
|
76
|
+
get_status: `Error fetching build: ${errorMessage}`,
|
|
77
|
+
get_changes: `Error fetching build changes: ${errorMessage}`,
|
|
78
|
+
};
|
|
79
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
100
80
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
81
|
+
});
|
|
82
|
+
// ─── pipelines_build_log ────────────────────────────────────────────────────
|
|
83
|
+
server.tool(PIPELINE_TOOLS.pipelines_build_log, "Retrieve build log data for a project. Use the action parameter to specify the operation.", {
|
|
84
|
+
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)."),
|
|
85
|
+
project: z.string().describe("Project ID or name."),
|
|
86
|
+
buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
|
|
87
|
+
logId: z.coerce.number().min(1).optional().describe("ID of the log to retrieve. Required for: get_content."),
|
|
88
|
+
startLine: z.coerce.number().optional().describe("Starting line number for the log content, defaults to 0. Used for: get_content."),
|
|
89
|
+
endLine: z.coerce.number().optional().describe("Ending line number for the log content, defaults to end of log. Used for: get_content."),
|
|
90
|
+
}, async ({ action, project, buildId, logId, startLine, endLine }) => {
|
|
91
|
+
try {
|
|
92
|
+
const connection = await connectionProvider();
|
|
93
|
+
const buildApi = await connection.getBuildApi();
|
|
94
|
+
if (action === "list") {
|
|
95
|
+
const logs = await buildApi.getBuildLogs(project, buildId);
|
|
96
|
+
return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
|
|
104
97
|
}
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
if (action === "get_content") {
|
|
99
|
+
if (!logId)
|
|
100
|
+
return { content: [{ type: "text", text: "logId is required for get_content" }], isError: true };
|
|
101
|
+
const logLines = await buildApi.getBuildLogLines(project, buildId, logId, startLine, endLine);
|
|
102
|
+
return createExternalContentResponse(logLines, "build log");
|
|
103
|
+
}
|
|
104
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
107
105
|
}
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
catch (error) {
|
|
107
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
108
|
+
const msgs = {
|
|
109
|
+
list: `Error fetching build log: ${errorMessage}`,
|
|
110
|
+
get_content: `Error fetching build log: ${errorMessage}`,
|
|
111
|
+
};
|
|
112
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
110
113
|
}
|
|
111
|
-
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
114
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
115
|
+
// ─── pipelines_definition ───────────────────────────────────────────────────
|
|
116
|
+
server.tool(PIPELINE_TOOLS.pipelines_definition, "Retrieve pipeline definition data for a project. Use the action parameter to specify the operation.", {
|
|
117
|
+
action: z
|
|
118
|
+
.enum(["list", "list_revisions"])
|
|
119
|
+
.describe("The action to perform. Options: list (list pipeline definitions with optional filters), list_revisions (list revision history for a pipeline definition)."),
|
|
120
|
+
project: z.string().describe("Project ID or name."),
|
|
121
|
+
definitionId: z.coerce.number().min(1).optional().describe("ID of the build definition. Required for: list_revisions."),
|
|
122
|
+
// list-specific
|
|
123
|
+
repositoryId: z.string().optional().describe("Repository ID to filter definitions. Can be a GUID or name (auto-resolved for TfsGit). Used for: list."),
|
|
124
|
+
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter definitions. Used for: list."),
|
|
125
|
+
name: z.string().optional().describe("Name filter for build definitions. Used for: list."),
|
|
126
|
+
path: z.string().optional().describe("Path filter for build definitions. Used for: list."),
|
|
127
|
+
queryOrder: z.string().optional().describe("Order in which definitions are returned (DefinitionQueryOrder values). Used for: list."),
|
|
128
|
+
top: z.number().optional().describe("Maximum number of definitions to return. Used for: list."),
|
|
129
|
+
continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list."),
|
|
130
|
+
minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter definitions. Used for: list."),
|
|
131
|
+
definitionIds: z.array(z.coerce.number().min(1)).optional().describe("Array of definition IDs to filter. Used for: list."),
|
|
132
|
+
builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date. Used for: list."),
|
|
133
|
+
notBuiltAfter: z.coerce.date().optional().describe("Return definitions without builds after this date. Used for: list."),
|
|
134
|
+
includeAllProperties: z.boolean().optional().describe("Whether to include all properties in results. Used for: list."),
|
|
135
|
+
includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition. Used for: list."),
|
|
136
|
+
taskIdFilter: z.string().optional().describe("Task ID to filter build definitions. Used for: list."),
|
|
137
|
+
processType: z.number().optional().describe("Process type to filter build definitions. Used for: list."),
|
|
138
|
+
yamlFilename: z.string().optional().describe("YAML filename to filter build definitions. Used for: list."),
|
|
139
|
+
}, async ({ action, project, definitionId, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
|
|
140
|
+
try {
|
|
141
|
+
const connection = await connectionProvider();
|
|
142
|
+
const buildApi = await connection.getBuildApi();
|
|
143
|
+
if (action === "list") {
|
|
144
|
+
let resolvedRepositoryId = repositoryId;
|
|
145
|
+
if (repositoryId) {
|
|
146
|
+
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);
|
|
147
|
+
if (!isGuid && (!repositoryType || repositoryType === "TfsGit")) {
|
|
148
|
+
const gitApi = await connection.getGitApi();
|
|
149
|
+
const repositories = await gitApi.getRepositories(project);
|
|
150
|
+
const repo = repositories?.find((r) => r.name === repositoryId);
|
|
151
|
+
if (!repo?.id) {
|
|
152
|
+
return { content: [{ type: "text", text: `Error: Repository '${repositoryId}' not found in project '${project}'.` }], isError: true };
|
|
153
|
+
}
|
|
154
|
+
resolvedRepositoryId = repo.id;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const defs = await buildApi.getDefinitions(project, name, resolvedRepositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
|
|
158
|
+
return { content: [{ type: "text", text: JSON.stringify(defs, null, 2) }] };
|
|
159
|
+
}
|
|
160
|
+
if (action === "list_revisions") {
|
|
161
|
+
if (!definitionId)
|
|
162
|
+
return { content: [{ type: "text", text: "definitionId is required for list_revisions" }], isError: true };
|
|
163
|
+
const revisions = await buildApi.getDefinitionRevisions(project, definitionId);
|
|
164
|
+
return { content: [{ type: "text", text: JSON.stringify(revisions, null, 2) }] };
|
|
165
|
+
}
|
|
166
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
170
|
+
const msgs = {
|
|
171
|
+
list: `Error fetching build definitions: ${errorMessage}`,
|
|
172
|
+
list_revisions: `Error fetching build definition revisions: ${errorMessage}`,
|
|
173
|
+
};
|
|
174
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
175
|
+
}
|
|
194
176
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}, async ({
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
177
|
+
// ─── pipelines_run ──────────────────────────────────────────────────────────
|
|
178
|
+
server.tool(PIPELINE_TOOLS.pipelines_run, "Retrieve pipeline run data for a project. Use the action parameter to specify the operation.", {
|
|
179
|
+
action: z.enum(["get", "list"]).describe("The action to perform. Options: get (get a single pipeline run), list (list runs for a pipeline)."),
|
|
180
|
+
project: z.string().describe("Project ID or name."),
|
|
181
|
+
pipelineId: z.coerce.number().min(1).describe("ID of the pipeline. Required for all actions."),
|
|
182
|
+
runId: z.coerce.number().min(1).optional().describe("ID of the run. Required for: get."),
|
|
183
|
+
}, async ({ action, project, pipelineId, runId }) => {
|
|
184
|
+
try {
|
|
185
|
+
const connection = await connectionProvider();
|
|
186
|
+
const pipelinesApi = await connection.getPipelinesApi();
|
|
187
|
+
if (action === "get") {
|
|
188
|
+
if (!runId)
|
|
189
|
+
return { content: [{ type: "text", text: "runId is required for get" }], isError: true };
|
|
190
|
+
const run = await pipelinesApi.getRun(project, pipelineId, runId);
|
|
191
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
192
|
+
}
|
|
193
|
+
if (action === "list") {
|
|
194
|
+
const runs = await pipelinesApi.listRuns(project, pipelineId);
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
|
|
196
|
+
}
|
|
197
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
201
|
+
const msgs = {
|
|
202
|
+
get: `Error fetching pipeline run: ${errorMessage}`,
|
|
203
|
+
list: `Error fetching pipeline runs: ${errorMessage}`,
|
|
204
|
+
};
|
|
205
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
206
|
+
}
|
|
208
207
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
208
|
+
// ─── pipelines_artifact ─────────────────────────────────────────────────────
|
|
209
|
+
server.tool(PIPELINE_TOOLS.pipelines_artifact, "Retrieve and download build artifacts. Use the action parameter to specify the operation.", {
|
|
210
|
+
action: z.enum(["list", "download"]).describe("The action to perform. Options: list (list artifacts for a build), download (download a named build artifact)."),
|
|
211
|
+
project: z.string().describe("Project ID or name."),
|
|
212
|
+
buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
|
|
213
|
+
artifactName: z.string().optional().describe("Name of the artifact. Required for: download."),
|
|
214
|
+
destinationPath: z.string().optional().describe("Relative local path to download the artifact to. If not provided, returns base64 content. Used for: download."),
|
|
215
|
+
}, async ({ action, project, buildId, artifactName, destinationPath }) => {
|
|
216
|
+
try {
|
|
217
|
+
// Validate artifact/path inputs before making any network calls
|
|
218
|
+
if (action === "download") {
|
|
219
|
+
if (!artifactName)
|
|
220
|
+
return { content: [{ type: "text", text: "artifactName is required for download" }], isError: true };
|
|
221
|
+
const hasUnsafePathSegment = (value) => value.split(/[\\/]+/).some((segment) => segment === "." || segment === "..");
|
|
222
|
+
const hasPathSeparators = (value) => /[\\/]/.test(value);
|
|
223
|
+
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
224
|
+
const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
|
|
225
|
+
if (hasUnsafePathSegment(artifactName) || hasPathSeparators(artifactName) || hasDriveLetter(artifactName) || isAbsolutePath(artifactName)) {
|
|
226
|
+
throw new Error("Invalid artifactName: artifactName must be a file name, not a path.");
|
|
227
|
+
}
|
|
228
|
+
if (destinationPath && (hasUnsafePathSegment(destinationPath) || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
|
|
229
|
+
throw new Error("Invalid destinationPath: use a relative path without path traversal.");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const connection = await connectionProvider();
|
|
233
|
+
const buildApi = await connection.getBuildApi();
|
|
234
|
+
if (action === "list") {
|
|
235
|
+
const artifacts = await buildApi.getArtifacts(project, buildId);
|
|
236
|
+
return { content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }] };
|
|
237
|
+
}
|
|
238
|
+
if (action === "download") {
|
|
239
|
+
const resolvedArtifactName = artifactName; // validated in pre-flight check above
|
|
240
|
+
const artifact = await buildApi.getArtifact(project, buildId, resolvedArtifactName);
|
|
241
|
+
if (!artifact) {
|
|
242
|
+
return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} not found in build ${buildId}.` }], isError: true };
|
|
243
|
+
}
|
|
244
|
+
const fileStream = await buildApi.getArtifactContentZip(project, buildId, resolvedArtifactName);
|
|
245
|
+
if (destinationPath) {
|
|
246
|
+
const fullDestinationPath = resolve(destinationPath);
|
|
247
|
+
mkdirSync(fullDestinationPath, { recursive: true });
|
|
248
|
+
const fileDestinationPath = join(fullDestinationPath, `${resolvedArtifactName}.zip`);
|
|
249
|
+
const writeStream = createWriteStream(fileDestinationPath);
|
|
250
|
+
await new Promise((resolve, reject) => {
|
|
251
|
+
fileStream.pipe(writeStream);
|
|
252
|
+
fileStream.on("end", () => resolve());
|
|
253
|
+
fileStream.on("error", (err) => reject(err));
|
|
254
|
+
});
|
|
255
|
+
return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} downloaded to ${destinationPath}.` }] };
|
|
256
|
+
}
|
|
257
|
+
const chunks = [];
|
|
258
|
+
await new Promise((resolve, reject) => {
|
|
259
|
+
fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
260
|
+
fileStream.on("end", () => resolve());
|
|
261
|
+
fileStream.on("error", (err) => reject(err));
|
|
262
|
+
});
|
|
263
|
+
const buffer = Buffer.concat(chunks);
|
|
264
|
+
const base64Data = buffer.toString("base64");
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: "resource", resource: { uri: `data:application/zip;base64,${base64Data}`, mimeType: "application/zip", text: base64Data } }],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
273
|
+
const msgs = {
|
|
274
|
+
list: `Error fetching artifacts: ${errorMessage}`,
|
|
275
|
+
download: `Error downloading artifact: ${errorMessage}`,
|
|
276
|
+
};
|
|
277
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
278
|
+
}
|
|
220
279
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
};
|
|
280
|
+
const variableSchema = z.object({
|
|
281
|
+
value: z.string().optional(),
|
|
282
|
+
isSecret: z.boolean().optional(),
|
|
231
283
|
});
|
|
232
284
|
const resourcesSchema = z.object({
|
|
233
|
-
builds: z
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.optional(),
|
|
238
|
-
|
|
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."),
|
|
285
|
+
builds: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
|
|
286
|
+
containers: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
|
|
287
|
+
packages: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
|
|
288
|
+
pipelines: z.record(z.string(), z.object({
|
|
289
|
+
runId: z.coerce.number().min(1).optional().describe("Id of the source pipeline run."),
|
|
290
|
+
version: z.string().optional(),
|
|
251
291
|
})),
|
|
252
292
|
repositories: z
|
|
253
|
-
.record(z.string()
|
|
293
|
+
.record(z.string(), z.object({
|
|
254
294
|
refName: z.string().describe("Reference name, e.g., refs/heads/main."),
|
|
255
295
|
token: z.string().optional(),
|
|
256
296
|
tokenType: z.string().optional(),
|
|
257
|
-
version: z.string().optional()
|
|
297
|
+
version: z.string().optional(),
|
|
258
298
|
}))
|
|
259
299
|
.optional(),
|
|
260
300
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
};
|
|
301
|
+
// ─── pipelines_write ────────────────────────────────────────────────────────
|
|
302
|
+
server.tool(PIPELINE_TOOLS.pipelines_write, "Write operations for pipelines and builds. Use the action parameter to specify the operation.", {
|
|
303
|
+
action: z
|
|
304
|
+
.enum(["run_pipeline", "create_pipeline", "update_build_stage"])
|
|
305
|
+
.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)."),
|
|
306
|
+
project: z.string().describe("Project ID or name."),
|
|
307
|
+
// run_pipeline params
|
|
308
|
+
pipelineId: z.coerce.number().min(1).optional().describe("ID of the pipeline to run. Required for: run_pipeline."),
|
|
309
|
+
pipelineVersion: z.coerce.number().min(1).optional().describe("Version of the pipeline to run. Used for: run_pipeline."),
|
|
310
|
+
previewRun: z.boolean().optional().describe("If true, returns the final YAML without creating a run. Used for: run_pipeline."),
|
|
311
|
+
resources: resourcesSchema.optional().describe("Resources to pass to the pipeline. Used for: run_pipeline."),
|
|
312
|
+
stagesToSkip: z.array(z.string()).optional().describe("Stages to skip. Used for: run_pipeline."),
|
|
313
|
+
templateParameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs. Used for: run_pipeline."),
|
|
314
|
+
variables: z.record(z.string(), variableSchema).optional().describe("Variables to pass to the pipeline. Used for: run_pipeline."),
|
|
315
|
+
yamlOverride: z.string().optional().describe("YAML override (only valid with previewRun). Used for: run_pipeline."),
|
|
316
|
+
// create_pipeline params
|
|
317
|
+
name: z.string().optional().describe("Name of the new pipeline. Required for: create_pipeline."),
|
|
318
|
+
folder: z.string().optional().describe("Folder path for the new pipeline. Used for: create_pipeline."),
|
|
319
|
+
yamlPath: z.string().optional().describe("Path to the YAML file in the repository. Required for: create_pipeline."),
|
|
320
|
+
repositoryType: z
|
|
321
|
+
.enum(getEnumKeys(RepositoryType))
|
|
322
|
+
.optional()
|
|
323
|
+
.describe("Type of the repository. Required for: create_pipeline."),
|
|
324
|
+
repositoryName: z.string().optional().describe("Name of the repository (for GitHub: owner/repo). Required for: create_pipeline."),
|
|
325
|
+
repositoryId: z.string().optional().describe("ID of the repository. Used for: create_pipeline."),
|
|
326
|
+
repositoryConnectionId: z.string().optional().describe("Service connection ID for GitHub repositories. Used for: create_pipeline."),
|
|
327
|
+
// update_build_stage params
|
|
328
|
+
buildId: z.coerce.number().min(1).optional().describe("ID of the build to update. Required for: update_build_stage."),
|
|
329
|
+
stageName: z.string().optional().describe("Name of the stage to update. Required for: update_build_stage."),
|
|
330
|
+
status: z
|
|
331
|
+
.enum(getEnumKeys(StageUpdateType))
|
|
332
|
+
.optional()
|
|
333
|
+
.describe("New status for the stage. Required for: update_build_stage."),
|
|
334
|
+
forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage. Used for: update_build_stage."),
|
|
335
|
+
}, async ({ action, project, pipelineId, pipelineVersion, previewRun, resources, stagesToSkip, templateParameters, variables, yamlOverride, name, folder, yamlPath, repositoryType, repositoryName, repositoryId, repositoryConnectionId, buildId, stageName, status, forceRetryAllJobs, }) => {
|
|
336
|
+
try {
|
|
337
|
+
if (action === "run_pipeline") {
|
|
338
|
+
if (!pipelineId)
|
|
339
|
+
return { content: [{ type: "text", text: "pipelineId is required for run_pipeline" }], isError: true };
|
|
340
|
+
if (!previewRun && yamlOverride)
|
|
341
|
+
throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
|
|
342
|
+
}
|
|
343
|
+
if (action === "create_pipeline") {
|
|
344
|
+
if (!name)
|
|
345
|
+
return { content: [{ type: "text", text: "name is required for create_pipeline" }], isError: true };
|
|
346
|
+
if (!yamlPath)
|
|
347
|
+
return { content: [{ type: "text", text: "yamlPath is required for create_pipeline" }], isError: true };
|
|
348
|
+
if (!repositoryType)
|
|
349
|
+
return { content: [{ type: "text", text: "repositoryType is required for create_pipeline" }], isError: true };
|
|
350
|
+
if (!repositoryName)
|
|
351
|
+
return { content: [{ type: "text", text: "repositoryName is required for create_pipeline" }], isError: true };
|
|
352
|
+
}
|
|
353
|
+
if (action === "update_build_stage") {
|
|
354
|
+
if (!buildId)
|
|
355
|
+
return { content: [{ type: "text", text: "buildId is required for update_build_stage" }], isError: true };
|
|
356
|
+
if (!stageName)
|
|
357
|
+
return { content: [{ type: "text", text: "stageName is required for update_build_stage" }], isError: true };
|
|
358
|
+
if (!status)
|
|
359
|
+
return { content: [{ type: "text", text: "status is required for update_build_stage" }], isError: true };
|
|
360
|
+
}
|
|
361
|
+
const connection = await connectionProvider();
|
|
362
|
+
if (action === "run_pipeline") {
|
|
363
|
+
const pipelinesApi = await connection.getPipelinesApi();
|
|
364
|
+
const runRequest = { previewRun, resources: { ...resources }, stagesToSkip, templateParameters, variables, yamlOverride };
|
|
365
|
+
const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, pipelineId, pipelineVersion);
|
|
366
|
+
if (pipelineRun.id === undefined)
|
|
367
|
+
throw new Error("Failed to get build ID from pipeline run");
|
|
368
|
+
return { content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }] };
|
|
369
|
+
}
|
|
370
|
+
if (action === "create_pipeline") {
|
|
371
|
+
const pipelinesApi = await connection.getPipelinesApi();
|
|
372
|
+
const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
|
|
373
|
+
const repositoryPayload = { type: repositoryType };
|
|
374
|
+
if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
|
|
375
|
+
repositoryPayload.id = repositoryId;
|
|
376
|
+
repositoryPayload.name = repositoryName;
|
|
377
|
+
}
|
|
378
|
+
else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
|
|
379
|
+
if (!repositoryConnectionId)
|
|
380
|
+
throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
|
|
381
|
+
repositoryPayload.connection = { id: repositoryConnectionId };
|
|
382
|
+
repositoryPayload.fullname = repositoryName;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
throw new Error("Unsupported repository type");
|
|
386
|
+
}
|
|
387
|
+
const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
|
|
388
|
+
const createParams = {
|
|
389
|
+
name,
|
|
390
|
+
folder: folder || "\\",
|
|
391
|
+
configuration: { type: yamlConfigurationType, path: yamlPath, repository: repositoryPayload, variables: undefined },
|
|
392
|
+
};
|
|
393
|
+
const newPipeline = await pipelinesApi.createPipeline(createParams, project);
|
|
394
|
+
return { content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }] };
|
|
395
|
+
}
|
|
396
|
+
if (action === "update_build_stage") {
|
|
397
|
+
const orgUrl = connection.serverUrl;
|
|
398
|
+
const endpoint = `${orgUrl}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/stages/${encodeURIComponent(stageName)}?api-version=${apiVersion}`;
|
|
399
|
+
const token = await tokenProvider();
|
|
400
|
+
const body = { forceRetryAllJobs, state: safeEnumConvert(StageUpdateType, status) };
|
|
401
|
+
const response = await fetch(endpoint, {
|
|
402
|
+
method: "PATCH",
|
|
403
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": userAgentProvider() },
|
|
404
|
+
body: JSON.stringify(body),
|
|
405
|
+
});
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
const errorText = await response.text();
|
|
408
|
+
throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
|
|
409
|
+
}
|
|
410
|
+
const updatedBuild = await response.text();
|
|
411
|
+
return { content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }] };
|
|
412
|
+
}
|
|
413
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
375
414
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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}.` }],
|
|
415
|
+
catch (error) {
|
|
416
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
417
|
+
const msgs = {
|
|
418
|
+
run_pipeline: `Error running pipeline: ${errorMessage}`,
|
|
419
|
+
create_pipeline: `Error creating pipeline: ${errorMessage}`,
|
|
420
|
+
update_build_stage: `Error updating build stage: ${errorMessage}`,
|
|
390
421
|
};
|
|
422
|
+
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
391
423
|
}
|
|
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
|
-
};
|
|
413
424
|
});
|
|
414
425
|
}
|
|
415
426
|
export { PIPELINE_TOOLS, configurePipelineTools };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.8.1-nightly.
|
|
1
|
+
export const packageVersion = "2.8.1-nightly.20260716";
|