@sonyjv/azure-devops-mcp 2.9.0-onprem.1

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.
@@ -0,0 +1,103 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { z } from "zod";
4
+ import { searchIdentities } from "./auth.js";
5
+ import { elicitProject } from "../shared/elicitations.js";
6
+ const CORE_TOOLS = {
7
+ list_project_teams: "core_list_project_teams",
8
+ list_projects: "core_list_projects",
9
+ get_identity_ids: "core_get_identity_ids",
10
+ };
11
+ function filterProjectsByName(projects, projectNameFilter) {
12
+ const lowerCaseFilter = projectNameFilter.toLowerCase();
13
+ return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
14
+ }
15
+ function configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider) {
16
+ server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for an Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
17
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
18
+ mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
19
+ top: z.coerce.number().optional().describe("The maximum number of teams to return. Defaults to 100."),
20
+ skip: z.coerce.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
21
+ }, async ({ project, mine, top, skip }) => {
22
+ try {
23
+ const connection = await connectionProvider();
24
+ const coreApi = await connection.getCoreApi();
25
+ let resolvedProject = project;
26
+ if (!resolvedProject) {
27
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list teams for.");
28
+ if ("response" in result)
29
+ return result.response;
30
+ resolvedProject = result.resolved;
31
+ }
32
+ const teams = await coreApi.getTeams(resolvedProject, mine, top, skip, false);
33
+ if (!teams) {
34
+ return { content: [{ type: "text", text: "No teams found" }], isError: true };
35
+ }
36
+ return {
37
+ content: [{ type: "text", text: JSON.stringify(teams, null, 2) }],
38
+ };
39
+ }
40
+ catch (error) {
41
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
42
+ return {
43
+ content: [{ type: "text", text: `Error fetching project teams: ${errorMessage}` }],
44
+ isError: true,
45
+ };
46
+ }
47
+ });
48
+ server.tool(CORE_TOOLS.list_projects, "Retrieve a list of projects in your Azure DevOps organization.", {
49
+ stateFilter: z.enum(["all", "wellFormed", "createPending", "deleted"]).default("wellFormed").describe("Filter projects by their state. Defaults to 'wellFormed'."),
50
+ top: z.coerce.number().optional().describe("The maximum number of projects to return. Defaults to 100."),
51
+ skip: z.coerce.number().optional().describe("The number of projects to skip for pagination. Defaults to 0."),
52
+ continuationToken: z.coerce.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
53
+ projectNameFilter: z.string().optional().describe("Filter projects by name. Supports partial matches."),
54
+ }, async ({ stateFilter, top, skip, continuationToken, projectNameFilter }) => {
55
+ try {
56
+ const connection = await connectionProvider();
57
+ const coreApi = await connection.getCoreApi();
58
+ const projects = await coreApi.getProjects(stateFilter, top, skip, continuationToken, false);
59
+ if (!projects) {
60
+ return { content: [{ type: "text", text: "No projects found" }], isError: true };
61
+ }
62
+ const filteredProject = projectNameFilter ? filterProjectsByName(projects, projectNameFilter) : projects;
63
+ return {
64
+ content: [{ type: "text", text: JSON.stringify(filteredProject, null, 2) }],
65
+ };
66
+ }
67
+ catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
69
+ return {
70
+ content: [{ type: "text", text: `Error fetching projects: ${errorMessage}` }],
71
+ isError: true,
72
+ };
73
+ }
74
+ });
75
+ server.tool(CORE_TOOLS.get_identity_ids, "Retrieve Azure DevOps identity IDs for a provided search filter.", {
76
+ searchFilter: z.string().describe("Search filter (unique name, display name, email) to retrieve identity IDs for."),
77
+ }, async ({ searchFilter }) => {
78
+ try {
79
+ const identities = await searchIdentities(searchFilter, tokenProvider, connectionProvider, userAgentProvider);
80
+ if (!identities || identities.value?.length === 0) {
81
+ return { content: [{ type: "text", text: "No identities found" }], isError: true };
82
+ }
83
+ const identitiesTrimmed = identities.value?.map((identity) => {
84
+ return {
85
+ id: identity.id,
86
+ displayName: identity.providerDisplayName,
87
+ descriptor: identity.descriptor,
88
+ };
89
+ });
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(identitiesTrimmed, null, 2) }],
92
+ };
93
+ }
94
+ catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
96
+ return {
97
+ content: [{ type: "text", text: `Error fetching identities: ${errorMessage}` }],
98
+ isError: true,
99
+ };
100
+ }
101
+ });
102
+ }
103
+ export { CORE_TOOLS, configureCoreTools };
@@ -0,0 +1,22 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ const MCP_APPS_TOOLS = {
4
+ ping: "mcp_apps_ping",
5
+ };
6
+ function configureMcpAppsTools(server) {
7
+ server.tool(MCP_APPS_TOOLS.ping, "A simple ping tool to verify that the mcp-apps domain is enabled.", {}, async () => {
8
+ try {
9
+ return {
10
+ content: [{ type: "text", text: "pong — mcp-apps domain is active" }],
11
+ };
12
+ }
13
+ catch (error) {
14
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
15
+ return {
16
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
17
+ isError: true,
18
+ };
19
+ }
20
+ });
21
+ }
22
+ export { configureMcpAppsTools, MCP_APPS_TOOLS };
@@ -0,0 +1,103 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { z } from "zod";
4
+ import { getEnumKeys } from "../utils.js";
5
+ import { RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
6
+ import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ // DTOs for the pipelines_write tool.
9
+ //
10
+ // Each action's inputs are declared once as a Zod "raw shape". The shapes are
11
+ // the single source of truth: the tool's input schema is composed from them,
12
+ // and the TypeScript argument types are derived via `z.infer` (no hand-written,
13
+ // drift-prone duplicate types). These types are safe to export — they are
14
+ // compile-time only and erased at runtime, so they have no effect on the MCP
15
+ // protocol or a local server.
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ export const variableSchema = z.object({
18
+ value: z.string().optional(),
19
+ isSecret: z.boolean().optional(),
20
+ });
21
+ export const resourcesSchema = z.object({
22
+ builds: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
23
+ containers: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
24
+ packages: z.record(z.string(), z.object({ version: z.string().optional() })).optional(),
25
+ pipelines: z
26
+ .record(z.string(), z.object({
27
+ runId: z.coerce.number().min(1).optional().describe("Id of the source pipeline run."),
28
+ version: z.string().optional(),
29
+ }))
30
+ .optional(),
31
+ repositories: z
32
+ .record(z.string(), z.object({
33
+ refName: z.string().describe("Reference name, e.g., refs/heads/main."),
34
+ token: z.string().optional(),
35
+ tokenType: z.string().optional(),
36
+ version: z.string().optional(),
37
+ }))
38
+ .optional(),
39
+ });
40
+ /** Fields shared by every write action. */
41
+ const projectShape = {
42
+ project: z.string().describe("Project ID or name."),
43
+ };
44
+ /** run_pipeline inputs. */
45
+ export const runPipelineShape = {
46
+ ...projectShape,
47
+ pipelineId: z.coerce.number().min(1).optional().describe("ID of the pipeline to run. Required for: run_pipeline."),
48
+ pipelineVersion: z.coerce.number().min(1).optional().describe("Version of the pipeline to run. Used for: run_pipeline."),
49
+ previewRun: z.boolean().optional().describe("If true, returns the final YAML without creating a run. Used for: run_pipeline."),
50
+ resources: resourcesSchema.optional().describe("Resources to pass to the pipeline. Used for: run_pipeline."),
51
+ stagesToSkip: z.array(z.string()).optional().describe("Stages to skip. Used for: run_pipeline."),
52
+ templateParameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs. Used for: run_pipeline."),
53
+ variables: z.record(z.string(), variableSchema).optional().describe("Variables to pass to the pipeline. Used for: run_pipeline."),
54
+ yamlOverride: z.string().optional().describe("YAML override (only valid with previewRun). Used for: run_pipeline."),
55
+ };
56
+ /** create_pipeline inputs. */
57
+ export const createPipelineShape = {
58
+ ...projectShape,
59
+ name: z.string().optional().describe("Pipeline name. Required for: create_pipeline, rename_pipeline."),
60
+ folder: z.string().optional().describe("Folder path for the new pipeline. Used for: create_pipeline."),
61
+ yamlPath: z.string().optional().describe("Path to the YAML file in the repository. Required for: create_pipeline."),
62
+ repositoryType: z
63
+ .enum(getEnumKeys(RepositoryType))
64
+ .optional()
65
+ .describe("Type of the repository. Required for: create_pipeline."),
66
+ repositoryName: z.string().optional().describe("Name of the repository (for GitHub: owner/repo). Required for: create_pipeline."),
67
+ repositoryId: z.string().optional().describe("ID of the repository. Used for: create_pipeline."),
68
+ repositoryConnectionId: z.string().optional().describe("Service connection ID for GitHub repositories. Used for: create_pipeline."),
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
+ };
76
+ /** update_build_stage inputs. */
77
+ export const updateBuildStageShape = {
78
+ ...projectShape,
79
+ buildId: z.coerce.number().min(1).optional().describe("ID of the build to update. Required for: update_build_stage."),
80
+ stageName: z.string().optional().describe("Name of the stage to update. Required for: update_build_stage."),
81
+ status: z
82
+ .enum(getEnumKeys(StageUpdateType))
83
+ .optional()
84
+ .describe("New status for the stage. Required for: update_build_stage."),
85
+ forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage. Used for: update_build_stage."),
86
+ };
87
+ /** The composed input shape for the grouped `pipelines_write` tool. */
88
+ export const pipelinesWriteShape = {
89
+ action: z
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)."),
92
+ ...runPipelineShape,
93
+ ...createPipelineShape,
94
+ ...renamePipelineShape,
95
+ ...updateBuildStageShape,
96
+ };
97
+ // Per-action schemas + inferred argument DTOs. `z.infer` keeps these types in
98
+ // lockstep with the schemas above.
99
+ export const runPipelineSchema = z.object(runPipelineShape);
100
+ export const createPipelineSchema = z.object(createPipelineShape);
101
+ export const renamePipelineSchema = z.object(renamePipelineShape);
102
+ export const updateBuildStageSchema = z.object(updateBuildStageShape);
103
+ export const pipelinesWriteSchema = z.object(pipelinesWriteShape);
@@ -0,0 +1,401 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
4
+ import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
5
+ import { z } from "zod";
6
+ import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
7
+ import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
8
+ import { mkdirSync, createWriteStream } from "fs";
9
+ import { createExternalContentResponse } from "../shared/content-safety.js";
10
+ import { join, posix, resolve, win32 } from "path";
11
+ import { pipelinesWriteShape } from "./pipelines.dto.js";
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
+ }
103
+ const pipelinesWriteErrorPrefixes = {
104
+ run_pipeline: "Error running pipeline: ",
105
+ create_pipeline: "Error creating pipeline: ",
106
+ rename_pipeline: "Error renaming pipeline: ",
107
+ update_build_stage: "Error updating build stage: ",
108
+ };
109
+ const PIPELINE_TOOLS = {
110
+ pipelines_build: "pipelines_build",
111
+ pipelines_build_log: "pipelines_build_log",
112
+ pipelines_definition: "pipelines_definition",
113
+ pipelines_run: "pipelines_run",
114
+ pipelines_artifact: "pipelines_artifact",
115
+ pipelines_write: "pipelines_write",
116
+ };
117
+ function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
118
+ // ─── pipelines_build ────────────────────────────────────────────────────────
119
+ server.tool(PIPELINE_TOOLS.pipelines_build, "Retrieve build data for a project. Use the action parameter to specify the operation.", {
120
+ action: z
121
+ .enum(["list", "get_status", "get_changes"])
122
+ .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)."),
123
+ project: z.string().describe("Project ID or name."),
124
+ buildId: z.coerce.number().min(1).optional().describe("ID of the build. Required for: get_status, get_changes."),
125
+ // list-specific
126
+ definitions: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter builds. Used for: list."),
127
+ queues: z.array(z.coerce.number().min(1)).optional().describe("Array of queue IDs to filter builds. Used for: list."),
128
+ buildNumber: z.string().optional().describe("Build number to filter builds. Used for: list."),
129
+ minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds. Used for: list."),
130
+ maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds. Used for: list."),
131
+ requestedFor: z.string().optional().describe("User ID or name who requested the build. Used for: list."),
132
+ reasonFilter: z.number().optional().describe("Reason filter (see BuildReason enum). Used for: list."),
133
+ statusFilter: z.number().optional().describe("Status filter (see BuildStatus enum). Used for: list."),
134
+ resultFilter: z.number().optional().describe("Result filter (see BuildResult enum). Used for: list."),
135
+ tagFilters: z.array(z.string()).optional().describe("Array of tags to filter builds. Used for: list."),
136
+ properties: z.array(z.string()).optional().describe("Array of property names to include in results. Used for: list."),
137
+ top: z.number().optional().describe("Maximum number of builds to return. Used for: list, get_changes."),
138
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list, get_changes."),
139
+ maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition. Used for: list."),
140
+ deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum). Used for: list."),
141
+ queryOrder: z.string().optional().describe("Order in which builds are returned (BuildQueryOrder values). Used for: list."),
142
+ branchName: z.string().optional().describe("Branch name to filter builds. Used for: list."),
143
+ buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of specific build IDs to retrieve. Used for: list."),
144
+ repositoryId: z.string().optional().describe("Repository ID to filter builds. Used for: list."),
145
+ repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter builds. Used for: list."),
146
+ // get_changes-specific
147
+ includeSourceChange: z.boolean().optional().describe("Whether to include source changes in results. Used for: get_changes."),
148
+ }, 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, }) => {
149
+ try {
150
+ const connection = await connectionProvider();
151
+ const buildApi = await connection.getBuildApi();
152
+ if (action === "list") {
153
+ 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);
154
+ return { content: [{ type: "text", text: JSON.stringify(builds, null, 2) }] };
155
+ }
156
+ if (action === "get_status") {
157
+ if (!buildId)
158
+ return { content: [{ type: "text", text: "buildId is required for get_status" }], isError: true };
159
+ const build = await buildApi.getBuildReport(project, buildId);
160
+ return { content: [{ type: "text", text: JSON.stringify(build, null, 2) }] };
161
+ }
162
+ if (action === "get_changes") {
163
+ if (!buildId)
164
+ return { content: [{ type: "text", text: "buildId is required for get_changes" }], isError: true };
165
+ const changes = await buildApi.getBuildChanges(project, buildId, continuationToken, top, includeSourceChange);
166
+ return { content: [{ type: "text", text: JSON.stringify(changes, null, 2) }] };
167
+ }
168
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
169
+ }
170
+ catch (error) {
171
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
172
+ const msgs = {
173
+ list: `Error fetching builds: ${errorMessage}`,
174
+ get_status: `Error fetching build: ${errorMessage}`,
175
+ get_changes: `Error fetching build changes: ${errorMessage}`,
176
+ };
177
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
178
+ }
179
+ });
180
+ // ─── pipelines_build_log ────────────────────────────────────────────────────
181
+ server.tool(PIPELINE_TOOLS.pipelines_build_log, "Retrieve build log data for a project. Use the action parameter to specify the operation.", {
182
+ 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)."),
183
+ project: z.string().describe("Project ID or name."),
184
+ buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
185
+ logId: z.coerce.number().min(1).optional().describe("ID of the log to retrieve. Required for: get_content."),
186
+ startLine: z.coerce.number().optional().describe("Starting line number for the log content, defaults to 0. Used for: get_content."),
187
+ endLine: z.coerce.number().optional().describe("Ending line number for the log content, defaults to end of log. Used for: get_content."),
188
+ }, async ({ action, project, buildId, logId, startLine, endLine }) => {
189
+ try {
190
+ const connection = await connectionProvider();
191
+ const buildApi = await connection.getBuildApi();
192
+ if (action === "list") {
193
+ const logs = await buildApi.getBuildLogs(project, buildId);
194
+ return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
195
+ }
196
+ if (action === "get_content") {
197
+ if (!logId)
198
+ return { content: [{ type: "text", text: "logId is required for get_content" }], isError: true };
199
+ const logLines = await buildApi.getBuildLogLines(project, buildId, logId, startLine, endLine);
200
+ return createExternalContentResponse(logLines, "build log");
201
+ }
202
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
203
+ }
204
+ catch (error) {
205
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
206
+ const msgs = {
207
+ list: `Error fetching build log: ${errorMessage}`,
208
+ get_content: `Error fetching build log: ${errorMessage}`,
209
+ };
210
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
211
+ }
212
+ });
213
+ // ─── pipelines_definition ───────────────────────────────────────────────────
214
+ server.tool(PIPELINE_TOOLS.pipelines_definition, "Retrieve pipeline definition data for a project. Use the action parameter to specify the operation.", {
215
+ action: z
216
+ .enum(["list", "list_revisions"])
217
+ .describe("The action to perform. Options: list (list pipeline definitions with optional filters), list_revisions (list revision history for a pipeline definition)."),
218
+ project: z.string().describe("Project ID or name."),
219
+ definitionId: z.coerce.number().min(1).optional().describe("ID of the build definition. Required for: list_revisions."),
220
+ // list-specific
221
+ repositoryId: z.string().optional().describe("Repository ID to filter definitions. Can be a GUID or name (auto-resolved for TfsGit). Used for: list."),
222
+ repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Repository type to filter definitions. Used for: list."),
223
+ name: z.string().optional().describe("Name filter for build definitions. Used for: list."),
224
+ path: z.string().optional().describe("Path filter for build definitions. Used for: list."),
225
+ queryOrder: z.string().optional().describe("Order in which definitions are returned (DefinitionQueryOrder values). Used for: list."),
226
+ top: z.number().optional().describe("Maximum number of definitions to return. Used for: list."),
227
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list."),
228
+ minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter definitions. Used for: list."),
229
+ definitionIds: z.array(z.coerce.number().min(1)).optional().describe("Array of definition IDs to filter. Used for: list."),
230
+ builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date. Used for: list."),
231
+ notBuiltAfter: z.coerce.date().optional().describe("Return definitions without builds after this date. Used for: list."),
232
+ includeAllProperties: z.boolean().optional().describe("Whether to include all properties in results. Used for: list."),
233
+ includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition. Used for: list."),
234
+ taskIdFilter: z.string().optional().describe("Task ID to filter build definitions. Used for: list."),
235
+ processType: z.number().optional().describe("Process type to filter build definitions. Used for: list."),
236
+ yamlFilename: z.string().optional().describe("YAML filename to filter build definitions. Used for: list."),
237
+ }, async ({ action, project, definitionId, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
238
+ try {
239
+ const connection = await connectionProvider();
240
+ const buildApi = await connection.getBuildApi();
241
+ if (action === "list") {
242
+ let resolvedRepositoryId = repositoryId;
243
+ if (repositoryId) {
244
+ 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);
245
+ if (!isGuid && (!repositoryType || repositoryType === "TfsGit")) {
246
+ const gitApi = await connection.getGitApi();
247
+ const repositories = await gitApi.getRepositories(project);
248
+ const repo = repositories?.find((r) => r.name === repositoryId);
249
+ if (!repo?.id) {
250
+ return { content: [{ type: "text", text: `Error: Repository '${repositoryId}' not found in project '${project}'.` }], isError: true };
251
+ }
252
+ resolvedRepositoryId = repo.id;
253
+ }
254
+ }
255
+ const defs = await buildApi.getDefinitions(project, name, resolvedRepositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
256
+ return { content: [{ type: "text", text: JSON.stringify(defs, null, 2) }] };
257
+ }
258
+ if (action === "list_revisions") {
259
+ if (!definitionId)
260
+ return { content: [{ type: "text", text: "definitionId is required for list_revisions" }], isError: true };
261
+ const revisions = await buildApi.getDefinitionRevisions(project, definitionId);
262
+ return { content: [{ type: "text", text: JSON.stringify(revisions, null, 2) }] };
263
+ }
264
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
265
+ }
266
+ catch (error) {
267
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
268
+ const msgs = {
269
+ list: `Error fetching build definitions: ${errorMessage}`,
270
+ list_revisions: `Error fetching build definition revisions: ${errorMessage}`,
271
+ };
272
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
273
+ }
274
+ });
275
+ // ─── pipelines_run ──────────────────────────────────────────────────────────
276
+ server.tool(PIPELINE_TOOLS.pipelines_run, "Retrieve pipeline run data for a project. Use the action parameter to specify the operation.", {
277
+ action: z.enum(["get", "list"]).describe("The action to perform. Options: get (get a single pipeline run), list (list runs for a pipeline)."),
278
+ project: z.string().describe("Project ID or name."),
279
+ pipelineId: z.coerce.number().min(1).describe("ID of the pipeline. Required for all actions."),
280
+ runId: z.coerce.number().min(1).optional().describe("ID of the run. Required for: get."),
281
+ }, async ({ action, project, pipelineId, runId }) => {
282
+ try {
283
+ const connection = await connectionProvider();
284
+ const pipelinesApi = await connection.getPipelinesApi();
285
+ if (action === "get") {
286
+ if (!runId)
287
+ return { content: [{ type: "text", text: "runId is required for get" }], isError: true };
288
+ const run = await pipelinesApi.getRun(project, pipelineId, runId);
289
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
290
+ }
291
+ if (action === "list") {
292
+ const runs = await pipelinesApi.listRuns(project, pipelineId);
293
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
294
+ }
295
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
296
+ }
297
+ catch (error) {
298
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
299
+ const msgs = {
300
+ get: `Error fetching pipeline run: ${errorMessage}`,
301
+ list: `Error fetching pipeline runs: ${errorMessage}`,
302
+ };
303
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
304
+ }
305
+ });
306
+ server.tool(PIPELINE_TOOLS.pipelines_artifact, "Retrieve and download build artifacts. Use the action parameter to specify the operation.", {
307
+ action: z.enum(["list", "download"]).describe("The action to perform. Options: list (list artifacts for a build), download (download a named build artifact)."),
308
+ project: z.string().describe("Project ID or name."),
309
+ buildId: z.coerce.number().min(1).describe("ID of the build. Required for all actions."),
310
+ artifactName: z.string().optional().describe("Name of the artifact. Required for: download."),
311
+ destinationPath: z.string().optional().describe("Relative local path to download the artifact to. If not provided, returns base64 content. Used for: download."),
312
+ }, async ({ action, project, buildId, artifactName, destinationPath }) => {
313
+ try {
314
+ // Validate artifact/path inputs before making any network calls
315
+ if (action === "download") {
316
+ if (!artifactName)
317
+ return { content: [{ type: "text", text: "artifactName is required for download" }], isError: true };
318
+ const hasUnsafePathSegment = (value) => value.split(/[\\/]+/).some((segment) => segment === "." || segment === "..");
319
+ const hasPathSeparators = (value) => /[\\/]/.test(value);
320
+ const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
321
+ const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
322
+ if (hasUnsafePathSegment(artifactName) || hasPathSeparators(artifactName) || hasDriveLetter(artifactName) || isAbsolutePath(artifactName)) {
323
+ throw new Error("Invalid artifactName: artifactName must be a file name, not a path.");
324
+ }
325
+ if (destinationPath && (hasUnsafePathSegment(destinationPath) || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
326
+ throw new Error("Invalid destinationPath: use a relative path without path traversal.");
327
+ }
328
+ }
329
+ const connection = await connectionProvider();
330
+ const buildApi = await connection.getBuildApi();
331
+ if (action === "list") {
332
+ const artifacts = await buildApi.getArtifacts(project, buildId);
333
+ return { content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }] };
334
+ }
335
+ if (action === "download") {
336
+ const resolvedArtifactName = artifactName; // validated in pre-flight check above
337
+ const artifact = await buildApi.getArtifact(project, buildId, resolvedArtifactName);
338
+ if (!artifact) {
339
+ return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} not found in build ${buildId}.` }], isError: true };
340
+ }
341
+ const fileStream = await buildApi.getArtifactContentZip(project, buildId, resolvedArtifactName);
342
+ if (destinationPath) {
343
+ const fullDestinationPath = resolve(destinationPath);
344
+ mkdirSync(fullDestinationPath, { recursive: true });
345
+ const fileDestinationPath = join(fullDestinationPath, `${resolvedArtifactName}.zip`);
346
+ const writeStream = createWriteStream(fileDestinationPath);
347
+ await new Promise((resolve, reject) => {
348
+ fileStream.pipe(writeStream);
349
+ fileStream.on("end", () => resolve());
350
+ fileStream.on("error", (err) => reject(err));
351
+ });
352
+ return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} downloaded to ${destinationPath}.` }] };
353
+ }
354
+ const chunks = [];
355
+ await new Promise((resolve, reject) => {
356
+ fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
357
+ fileStream.on("end", () => resolve());
358
+ fileStream.on("error", (err) => reject(err));
359
+ });
360
+ const buffer = Buffer.concat(chunks);
361
+ const base64Data = buffer.toString("base64");
362
+ return {
363
+ content: [{ type: "resource", resource: { uri: `data:application/zip;base64,${base64Data}`, mimeType: "application/zip", text: base64Data } }],
364
+ };
365
+ }
366
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
367
+ }
368
+ catch (error) {
369
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
370
+ const msgs = {
371
+ list: `Error fetching artifacts: ${errorMessage}`,
372
+ download: `Error downloading artifact: ${errorMessage}`,
373
+ };
374
+ return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
375
+ }
376
+ });
377
+ // ─── pipelines_write ────────────────────────────────────────────────────────
378
+ server.tool(PIPELINE_TOOLS.pipelines_write, "Write operations for pipelines and builds. Use the action parameter to specify the operation.", pipelinesWriteShape, async (args) => {
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
+ }
399
+ });
400
+ }
401
+ export { PIPELINE_TOOLS, configurePipelineTools, runPipeline, createPipeline, renamePipeline, updateBuildStage };