@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,345 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { z } from "zod";
4
+ import { TreeStructureGroup, TreeNodeStructureType } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
5
+ import { elicitProject, elicitTeam } from "../shared/elicitations.js";
6
+ const WORK_TOOLS = {
7
+ work: "work",
8
+ work_iteration_write: "work_iteration_write",
9
+ work_capacity_write: "work_capacity_write",
10
+ };
11
+ function configureWorkTools(server, _, connectionProvider) {
12
+ server.tool(WORK_TOOLS.work, "Retrieve work-related data for a project or team. Use the action parameter to specify the operation.", {
13
+ action: z
14
+ .enum(["list_iterations", "list_team_iterations", "get_team_settings", "get_team_capacity", "get_iteration_capacities"])
15
+ .describe("The action to perform. Options: list_iterations (list all iterations in a project), list_team_iterations (list iterations assigned to a team), get_team_settings (get team settings including default iteration and area path), get_team_capacity (get team capacity for an iteration), get_iteration_capacities (get capacity for all teams in an iteration)."),
16
+ 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."),
17
+ team: z
18
+ .string()
19
+ .optional()
20
+ .describe("The name or ID of the Azure DevOps team. Required for list_team_iterations, get_team_settings, and get_team_capacity. Reuse from prior context if already known."),
21
+ iterationId: z.string().optional().describe("The Iteration ID. Required for get_team_capacity and get_iteration_capacities."),
22
+ timeframe: z.enum(["current"]).optional().describe("The timeframe for list_team_iterations. Only 'current' is supported."),
23
+ depth: z.coerce.number().default(2).describe("Depth of children to fetch. Used for list_iterations. Defaults to 2."),
24
+ excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and their children, to exclude from results. Used for list_iterations."),
25
+ }, async ({ action, project, team, iterationId, timeframe, depth, excludedIds }) => {
26
+ try {
27
+ const connection = await connectionProvider();
28
+ let resolvedProject = project;
29
+ if (action === "list_team_iterations") {
30
+ if (!resolvedProject) {
31
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list team iterations for.");
32
+ if ("response" in result)
33
+ return result.response;
34
+ resolvedProject = result.resolved;
35
+ }
36
+ let resolvedTeam = team;
37
+ if (!resolvedTeam) {
38
+ const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list iterations for.");
39
+ if ("response" in result)
40
+ return result.response;
41
+ resolvedTeam = result.resolved;
42
+ }
43
+ const workApi = await connection.getWorkApi();
44
+ const iterations = await workApi.getTeamIterations({ project: resolvedProject, team: resolvedTeam }, timeframe);
45
+ if (!iterations) {
46
+ return { content: [{ type: "text", text: "No iterations found" }], isError: true };
47
+ }
48
+ return {
49
+ content: [
50
+ { type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
51
+ { type: "text", text: JSON.stringify(iterations, null, 2) },
52
+ ],
53
+ };
54
+ }
55
+ if (action === "list_iterations") {
56
+ if (!resolvedProject) {
57
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list iterations for.");
58
+ if ("response" in result)
59
+ return result.response;
60
+ resolvedProject = result.resolved;
61
+ }
62
+ const workItemTrackingApi = await connection.getWorkItemTrackingApi();
63
+ const effectiveDepth = depth ?? 1;
64
+ const results = await workItemTrackingApi.getClassificationNodes(resolvedProject, [], effectiveDepth);
65
+ if (!results) {
66
+ return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
67
+ }
68
+ let filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
69
+ if (excludedIds && excludedIds.length > 0) {
70
+ const filterOutIds = (nodes) => {
71
+ return nodes
72
+ .filter((node) => !node.id || !excludedIds.includes(node.id))
73
+ .map((node) => {
74
+ if (node.children && node.children.length > 0) {
75
+ return {
76
+ ...node,
77
+ children: filterOutIds(node.children),
78
+ };
79
+ }
80
+ return node;
81
+ });
82
+ };
83
+ filteredResults = filterOutIds(filteredResults);
84
+ }
85
+ if (filteredResults.length === 0) {
86
+ return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
87
+ }
88
+ return {
89
+ content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
90
+ };
91
+ }
92
+ if (action === "get_team_settings") {
93
+ if (!resolvedProject) {
94
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team settings for.");
95
+ if ("response" in result)
96
+ return result.response;
97
+ resolvedProject = result.resolved;
98
+ }
99
+ let resolvedTeam = team;
100
+ if (!resolvedTeam) {
101
+ const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to get settings for.");
102
+ if ("response" in result)
103
+ return result.response;
104
+ resolvedTeam = result.resolved;
105
+ }
106
+ const workApi = await connection.getWorkApi();
107
+ const teamContext = { project: resolvedProject, team: resolvedTeam };
108
+ const teamSettings = await workApi.getTeamSettings(teamContext);
109
+ if (!teamSettings) {
110
+ return { content: [{ type: "text", text: "No team settings found" }], isError: true };
111
+ }
112
+ const teamFieldValues = await workApi.getTeamFieldValues(teamContext);
113
+ const settingsResult = {
114
+ backlogIteration: teamSettings.backlogIteration,
115
+ defaultIteration: teamSettings.defaultIteration,
116
+ defaultIterationMacro: teamSettings.defaultIterationMacro,
117
+ backlogVisibilities: teamSettings.backlogVisibilities,
118
+ bugsBehavior: teamSettings.bugsBehavior,
119
+ workingDays: teamSettings.workingDays,
120
+ defaultAreaPath: teamFieldValues?.defaultValue,
121
+ areaPathField: teamFieldValues?.field,
122
+ areaPaths: teamFieldValues?.values,
123
+ };
124
+ return {
125
+ content: [
126
+ { type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
127
+ { type: "text", text: JSON.stringify(settingsResult, null, 2) },
128
+ ],
129
+ };
130
+ }
131
+ if (action === "get_team_capacity") {
132
+ if (!resolvedProject) {
133
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team capacity for.");
134
+ if ("response" in result)
135
+ return result.response;
136
+ resolvedProject = result.resolved;
137
+ }
138
+ if (!team) {
139
+ return { content: [{ type: "text", text: "Team is required for get_team_capacity" }], isError: true };
140
+ }
141
+ if (!iterationId) {
142
+ return { content: [{ type: "text", text: "iterationId is required for get_team_capacity" }], isError: true };
143
+ }
144
+ const workApi = await connection.getWorkApi();
145
+ const teamContext = { project: resolvedProject, team };
146
+ const rawResults = await workApi.getCapacitiesWithIdentityRefAndTotals(teamContext, iterationId);
147
+ if (!rawResults || rawResults.teamMembers?.length === 0) {
148
+ return { content: [{ type: "text", text: "No team capacity assigned to the team" }], isError: true };
149
+ }
150
+ const simplifiedResults = {
151
+ ...rawResults,
152
+ teamMembers: (rawResults.teamMembers || []).map((member) => {
153
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
154
+ const { url, ...rest } = member;
155
+ return {
156
+ ...rest,
157
+ teamMember: member.teamMember
158
+ ? {
159
+ displayName: member.teamMember.displayName,
160
+ id: member.teamMember.id,
161
+ uniqueName: member.teamMember.uniqueName,
162
+ }
163
+ : undefined,
164
+ };
165
+ }),
166
+ };
167
+ return {
168
+ content: [{ type: "text", text: JSON.stringify(simplifiedResults, null, 2) }],
169
+ };
170
+ }
171
+ if (action === "get_iteration_capacities") {
172
+ if (!resolvedProject) {
173
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to get iteration capacities for.");
174
+ if ("response" in result)
175
+ return result.response;
176
+ resolvedProject = result.resolved;
177
+ }
178
+ if (!iterationId) {
179
+ return { content: [{ type: "text", text: "iterationId is required for get_iteration_capacities" }], isError: true };
180
+ }
181
+ const workApi = await connection.getWorkApi();
182
+ const rawResults = await workApi.getTotalIterationCapacities(resolvedProject, iterationId);
183
+ if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
184
+ return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
185
+ }
186
+ return {
187
+ content: [{ type: "text", text: JSON.stringify(rawResults, null, 2) }],
188
+ };
189
+ }
190
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
191
+ }
192
+ catch (error) {
193
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
194
+ const actionErrorMessages = {
195
+ list_team_iterations: `Error fetching team iterations: ${errorMessage}`,
196
+ list_iterations: `Error fetching iterations: ${errorMessage}`,
197
+ get_team_settings: `Error fetching team settings: ${errorMessage}`,
198
+ get_team_capacity: `Error getting team capacity: ${errorMessage}`,
199
+ get_iteration_capacities: `Error getting iteration capacities: ${errorMessage}`,
200
+ };
201
+ return {
202
+ content: [{ type: "text", text: actionErrorMessages[action] ?? `Error: ${errorMessage}` }],
203
+ isError: true,
204
+ };
205
+ }
206
+ });
207
+ server.tool(WORK_TOOLS.work_iteration_write, "Create or assign iterations in an Azure DevOps project. Use the action parameter to specify the operation.", {
208
+ action: z.enum(["create", "assign"]).describe("The action to perform. 'create' creates new iterations in the project; 'assign' assigns existing iterations to a team."),
209
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
210
+ team: z.string().optional().describe("The name or ID of the Azure DevOps team. Required for assign."),
211
+ iterations: z
212
+ .array(z.object({
213
+ iterationName: z.string().optional().describe("The name of the iteration to create. Used for create."),
214
+ startDate: z.string().optional().describe("The start date of the iteration in ISO format (e.g., '2023-01-01T00:00:00Z'). Used for create."),
215
+ finishDate: z.string().optional().describe("The finish date of the iteration in ISO format (e.g., '2023-01-31T23:59:59Z'). Used for create."),
216
+ identifier: z.string().optional().describe("The identifier of the iteration to assign. Used for assign."),
217
+ path: z.string().optional().describe("The path of the iteration to assign, e.g., 'Project/Iteration'. Used for assign."),
218
+ }))
219
+ .describe("An array of iterations to process. For create: provide iterationName and optional dates. For assign: provide identifier and path."),
220
+ }, async ({ action, project, team, iterations }) => {
221
+ try {
222
+ const connection = await connectionProvider();
223
+ if (action === "create") {
224
+ const workItemTrackingApi = await connection.getWorkItemTrackingApi();
225
+ const results = [];
226
+ for (const { iterationName, startDate, finishDate } of iterations) {
227
+ if (!iterationName)
228
+ continue;
229
+ const iteration = await workItemTrackingApi.createOrUpdateClassificationNode({
230
+ name: iterationName,
231
+ attributes: {
232
+ startDate: startDate ? new Date(startDate) : undefined,
233
+ finishDate: finishDate ? new Date(finishDate) : undefined,
234
+ },
235
+ }, project, TreeStructureGroup.Iterations);
236
+ if (iteration) {
237
+ results.push(iteration);
238
+ }
239
+ }
240
+ if (results.length === 0) {
241
+ return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
242
+ }
243
+ return {
244
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
245
+ };
246
+ }
247
+ if (action === "assign") {
248
+ if (!team) {
249
+ return { content: [{ type: "text", text: "Team is required for assign" }], isError: true };
250
+ }
251
+ const workApi = await connection.getWorkApi();
252
+ const teamContext = { project, team };
253
+ const results = [];
254
+ for (const { identifier, path } of iterations) {
255
+ if (!identifier || !path)
256
+ continue;
257
+ const assignment = await workApi.postTeamIteration({ path: path, id: identifier }, teamContext);
258
+ if (assignment) {
259
+ results.push(assignment);
260
+ }
261
+ }
262
+ if (results.length === 0) {
263
+ return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
264
+ }
265
+ return {
266
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
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 actionErrorMessages = {
274
+ create: `Error creating iterations: ${errorMessage}`,
275
+ assign: `Error assigning iterations: ${errorMessage}`,
276
+ };
277
+ return {
278
+ content: [{ type: "text", text: actionErrorMessages[action] ?? `Error: ${errorMessage}` }],
279
+ isError: true,
280
+ };
281
+ }
282
+ });
283
+ server.tool(WORK_TOOLS.work_capacity_write, "Update the team capacity of a team member for a specific iteration in a project.", {
284
+ action: z.literal("update").describe("The action to perform. Only 'update' is supported."),
285
+ project: z.string().describe("The name or Id of the Azure DevOps project."),
286
+ team: z.string().describe("The name or Id of the Azure DevOps team."),
287
+ teamMemberId: z.string().describe("The team member Id for the specific team member."),
288
+ iterationId: z.string().describe("The Iteration Id to update the capacity for."),
289
+ activities: z
290
+ .array(z.object({
291
+ name: z.string().describe("The name of the activity (e.g., 'Development')."),
292
+ capacityPerDay: z.number().describe("The capacity per day for this activity."),
293
+ }))
294
+ .describe("Array of activities and their daily capacities for the team member."),
295
+ daysOff: z
296
+ .array(z.object({
297
+ start: z.string().describe("Start date of the day off in ISO format."),
298
+ end: z.string().describe("End date of the day off in ISO format."),
299
+ }))
300
+ .optional()
301
+ .describe("Array of days off for the team member, each with a start and end date in ISO format."),
302
+ }, async ({ project, team, teamMemberId, iterationId, activities, daysOff }) => {
303
+ try {
304
+ const connection = await connectionProvider();
305
+ const workApi = await connection.getWorkApi();
306
+ const teamContext = { project, team };
307
+ const capacityPatch = {
308
+ activities: activities.map((a) => ({
309
+ name: a.name,
310
+ capacityPerDay: a.capacityPerDay,
311
+ })),
312
+ daysOff: (daysOff || []).map((d) => ({
313
+ start: new Date(d.start),
314
+ end: new Date(d.end),
315
+ })),
316
+ };
317
+ const updatedCapacity = await workApi.updateCapacityWithIdentityRef(capacityPatch, teamContext, iterationId, teamMemberId);
318
+ if (!updatedCapacity) {
319
+ return { content: [{ type: "text", text: "Failed to update team member capacity" }], isError: true };
320
+ }
321
+ const simplifiedResult = {
322
+ teamMember: updatedCapacity.teamMember
323
+ ? {
324
+ displayName: updatedCapacity.teamMember.displayName,
325
+ id: updatedCapacity.teamMember.id,
326
+ uniqueName: updatedCapacity.teamMember.uniqueName,
327
+ }
328
+ : undefined,
329
+ activities: updatedCapacity.activities,
330
+ daysOff: updatedCapacity.daysOff,
331
+ };
332
+ return {
333
+ content: [{ type: "text", text: JSON.stringify(simplifiedResult, null, 2) }],
334
+ };
335
+ }
336
+ catch (error) {
337
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
338
+ return {
339
+ content: [{ type: "text", text: `Error updating team capacity: ${errorMessage}` }],
340
+ isError: true,
341
+ };
342
+ }
343
+ });
344
+ }
345
+ export { WORK_TOOLS, configureWorkTools };
package/dist/tools.js ADDED
@@ -0,0 +1,31 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { Domain } from "./shared/domains.js";
4
+ import { configureAdvSecTools } from "./tools/advanced-security.js";
5
+ import { configureMcpAppsTools } from "./tools/mcp-apps.js";
6
+ import { configurePipelineTools } from "./tools/pipelines.js";
7
+ import { configureCoreTools } from "./tools/core.js";
8
+ import { configureRepoTools } from "./tools/repositories.js";
9
+ import { configureSearchTools } from "./tools/search.js";
10
+ import { configureTestPlanTools } from "./tools/test-plans.js";
11
+ import { configureWikiTools } from "./tools/wiki.js";
12
+ import { configureWorkTools } from "./tools/work.js";
13
+ import { configureWorkItemTools } from "./tools/work-items.js";
14
+ function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
15
+ const configureIfDomainEnabled = (domain, configureFn) => {
16
+ if (enabledDomains.has(domain)) {
17
+ configureFn();
18
+ }
19
+ };
20
+ configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
21
+ configureIfDomainEnabled(Domain.MCP_APPS, () => configureMcpAppsTools(server));
22
+ configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
23
+ configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
24
+ configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
25
+ configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
26
+ configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
27
+ configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
28
+ configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
+ configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
30
+ }
31
+ export { configureAllTools };
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ class UserAgentComposer {
4
+ _userAgent;
5
+ _mcpClientInfoAppended;
6
+ constructor(packageVersion) {
7
+ this._userAgent = `AzureDevOps.MCP/${packageVersion} (local)`;
8
+ this._mcpClientInfoAppended = false;
9
+ }
10
+ get userAgent() {
11
+ return this._userAgent;
12
+ }
13
+ appendMcpClientInfo(info) {
14
+ if (!this._mcpClientInfoAppended && info && info.name && info.version) {
15
+ this._userAgent += ` ${info.name}/${info.version}`;
16
+ this._mcpClientInfoAppended = true;
17
+ }
18
+ }
19
+ }
20
+ export { UserAgentComposer };
package/dist/utils.js ADDED
@@ -0,0 +1,173 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ export const apiVersion = "7.2-preview.1";
4
+ export const batchApiVersion = "5.0";
5
+ export const markdownCommentsApiVersion = "7.2-preview.4";
6
+ /**
7
+ * Returns the user-supplied CLI arguments.
8
+ *
9
+ * The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
10
+ * and on Electron hosts alike, so the first two entries are always dropped.
11
+ *
12
+ * Do not replace this with yargs' `hideBin`: it drops a single entry whenever
13
+ * `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
14
+ * host the script path survives and is parsed as the organization name.
15
+ */
16
+ export function getCliArgs(argv = process.argv) {
17
+ return argv.slice(2);
18
+ }
19
+ export function createEnumMapping(enumObject) {
20
+ const mapping = {};
21
+ for (const [key, value] of Object.entries(enumObject)) {
22
+ if (typeof key === "string" && typeof value === "number") {
23
+ mapping[key.toLowerCase()] = value;
24
+ }
25
+ }
26
+ return mapping;
27
+ }
28
+ export function mapStringToEnum(value, enumObject, defaultValue) {
29
+ if (!value)
30
+ return defaultValue;
31
+ const enumMapping = createEnumMapping(enumObject);
32
+ return enumMapping[value.toLowerCase()] ?? defaultValue;
33
+ }
34
+ /**
35
+ * Maps an array of strings to an array of enum values, filtering out invalid values.
36
+ * @param values Array of string values to map
37
+ * @param enumObject The enum object to map to
38
+ * @returns Array of valid enum values
39
+ */
40
+ export function mapStringArrayToEnum(values, enumObject) {
41
+ if (!values)
42
+ return [];
43
+ return values.map((value) => mapStringToEnum(value, enumObject)).filter((v) => v !== undefined);
44
+ }
45
+ /**
46
+ * Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
47
+ * This ensures that enum schemas generate string values rather than numeric values.
48
+ * @param enumObject The TypeScript enum object
49
+ * @returns Array of string keys from the enum
50
+ */
51
+ export function getEnumKeys(enumObject) {
52
+ return Object.keys(enumObject).filter((key) => isNaN(Number(key)));
53
+ }
54
+ /**
55
+ * Safely converts a string enum key to its corresponding enum value.
56
+ * Validates that the key exists in the enum before conversion.
57
+ * @param enumObject The TypeScript enum object
58
+ * @param key The string key to convert
59
+ * @returns The enum value if key is valid, undefined otherwise
60
+ */
61
+ export function safeEnumConvert(enumObject, key) {
62
+ if (!key)
63
+ return undefined;
64
+ const validKeys = getEnumKeys(enumObject);
65
+ if (!validKeys.includes(key)) {
66
+ return undefined;
67
+ }
68
+ return enumObject[key];
69
+ }
70
+ /**
71
+ * Encodes `>` and `<` for Markdown formatted fields.
72
+ *
73
+ * @param value The text value to encode
74
+ * @param format The format of the field ('Markdown' or 'Html')
75
+ * @returns The encoded text, or original text if format is not Markdown
76
+ */
77
+ export function encodeFormattedValue(value, format) {
78
+ if (!value || format !== "Markdown")
79
+ return value;
80
+ const result = value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
81
+ return result;
82
+ }
83
+ /**
84
+ * Detects whether a string returned from an ADO API stream is actually an error
85
+ * response serialized as JSON (e.g. a 404 GitItemNotFoundException or
86
+ * WikiPageNotFoundException) rather than real content.
87
+ *
88
+ * The ADO Node API client swallows non-2xx HTTP responses and delivers the
89
+ * error body as a stream, so callers must check explicitly after reading.
90
+ *
91
+ * @returns The human-readable error message extracted from the JSON, or null if
92
+ * the content is not an ADO error response.
93
+ */
94
+ export function extractAdoStreamError(content) {
95
+ try {
96
+ const json = JSON.parse(content.trim());
97
+ if (json && typeof json.typeName === "string" && typeof json.message === "string") {
98
+ return json.message;
99
+ }
100
+ }
101
+ catch {
102
+ // Not JSON — not an ADO error response.
103
+ }
104
+ return null;
105
+ }
106
+ /**
107
+ * Extracts the Azure DevOps organization identifier from a URL.
108
+ *
109
+ * Only recognized Azure DevOps hosts are accepted; any other host returns null
110
+ * so that callers can treat unrecognized URLs as a boundary violation.
111
+ *
112
+ * Supports both modern and legacy organization URL forms:
113
+ * - https://dev.azure.com/{org}/... -> org is the first path segment
114
+ * - https://{org}.visualstudio.com/... -> org is the host subdomain
115
+ *
116
+ * @param url Any Azure DevOps URL (e.g. a wiki page link or a connection serverUrl).
117
+ * @returns The lowercased organization name, or null if it cannot be determined.
118
+ */
119
+ export function getOrgFromUrl(url) {
120
+ try {
121
+ const u = new URL(url);
122
+ const host = u.hostname.toLowerCase();
123
+ if (host === "visualstudio.com" || host.endsWith(".visualstudio.com")) {
124
+ const subdomain = host.split(".")[0];
125
+ return subdomain && subdomain !== "visualstudio" ? subdomain : null;
126
+ }
127
+ if (host === "dev.azure.com" || host.endsWith(".dev.azure.com")) {
128
+ const firstSegment = u.pathname.split("/").filter(Boolean)[0];
129
+ return firstSegment ? firstSegment.toLowerCase() : null;
130
+ }
131
+ return null;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ /**
138
+ * Resolves the CLI `organization` argument to an Azure DevOps connection URL.
139
+ *
140
+ * Accepts either a bare Azure DevOps Services organization name (e.g. "contoso"),
141
+ * or a full base URL, which also covers on-premises Azure DevOps Server / TFS
142
+ * collections (e.g. "http://tfsserver:8080/tfs/DefaultCollection").
143
+ *
144
+ * `cloudOrgName` is non-null only when the resolved URL is a recognized Azure
145
+ * DevOps Services host (see {@link getOrgFromUrl}) — callers use it to gate
146
+ * cloud-only behavior (AAD tenant discovery, PAT host restrictions) that doesn't
147
+ * apply to on-premises servers.
148
+ *
149
+ * @param organizationArg The raw `organization` CLI argument.
150
+ * @returns The resolved connection URL and, when applicable, the cloud org name.
151
+ */
152
+ export function resolveOrgUrl(organizationArg) {
153
+ if (/^https?:\/\//i.test(organizationArg)) {
154
+ const orgUrl = organizationArg.replace(/\/+$/, "");
155
+ return { orgUrl, cloudOrgName: getOrgFromUrl(orgUrl) };
156
+ }
157
+ return { orgUrl: `https://dev.azure.com/${organizationArg}`, cloudOrgName: organizationArg };
158
+ }
159
+ /**
160
+ * Convert a Node.js ReadableStream to a string.
161
+ * Shared utility for consistent stream handling across tools.
162
+ */
163
+ export function streamToString(stream) {
164
+ return new Promise((resolve, reject) => {
165
+ let data = "";
166
+ stream.setEncoding("utf8");
167
+ stream.on("data", (chunk) => {
168
+ data += chunk;
169
+ });
170
+ stream.on("error", reject);
171
+ stream.on("end", () => resolve(data));
172
+ });
173
+ }
@@ -0,0 +1 @@
1
+ export const packageVersion = "2.9.0-onprem.1";
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@sonyjv/azure-devops-mcp",
3
+ "version": "2.9.0-onprem.1",
4
+ "mcpName": "io.github.sonyjv/azure-devops-mcp",
5
+ "description": "MCP server for interacting with Azure DevOps",
6
+ "license": "MIT",
7
+ "author": "Microsoft Corporation",
8
+ "homepage": "https://github.com/sonyjv/azure-devops-mcp",
9
+ "bugs": "https://github.com/sonyjv/azure-devops-mcp/issues",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/sonyjv/azure-devops-mcp.git"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "mcp-server-azuredevops": "dist/index.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
26
+ "validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
27
+ "build": "tsc && shx chmod +x dist/*.js",
28
+ "prepare": "npm run build && husky",
29
+ "watch": "tsc --watch",
30
+ "inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector@0.21.0 node dist/index.js",
31
+ "start": "node -r tsconfig-paths/register dist/index.js",
32
+ "eslint": "eslint",
33
+ "eslint-fix": "eslint --fix",
34
+ "format": "prettier --write .",
35
+ "format-check": "prettier --check .",
36
+ "clean": "shx rm -rf dist",
37
+ "test": "jest"
38
+ },
39
+ "dependencies": {
40
+ "@azure/identity": "^4.13.0",
41
+ "@azure/logger": "^1.3.0",
42
+ "@azure/msal-node": "^5.5.0",
43
+ "@azure/msal-node-extensions": "^5.3.5",
44
+ "@modelcontextprotocol/sdk": "1.29.0",
45
+ "azure-devops-extension-api": "^5.272.3",
46
+ "azure-devops-extension-sdk": "^4.0.2",
47
+ "azure-devops-node-api": "^15.1.2",
48
+ "open": "^10.2.0",
49
+ "winston": "^3.18.3",
50
+ "yargs": "^18.0.0",
51
+ "zod": "^3.25.63",
52
+ "zod-to-json-schema": "^3.24.5"
53
+ },
54
+ "devDependencies": {
55
+ "@types/jest": "^30.0.0",
56
+ "@types/node": "^22.19.1",
57
+ "eslint-config-prettier": "10.1.8",
58
+ "eslint-plugin-header": "^3.1.1",
59
+ "glob": "^13.0.0",
60
+ "husky": "^9.1.7",
61
+ "jest": "^30.0.2",
62
+ "jest-extended": "^7.0.0",
63
+ "lint-staged": "^17.0.0",
64
+ "prettier": "3.9.5",
65
+ "shx": "^0.4.0",
66
+ "ts-jest": "^29.4.6",
67
+ "tsconfig-paths": "^4.2.0",
68
+ "typescript": "^5.9.3",
69
+ "typescript-eslint": "^8.54.0"
70
+ },
71
+ "lint-staged": {
72
+ "**/*.(js|ts|jsx|tsx|json|css|md)": [
73
+ "npm run format"
74
+ ]
75
+ },
76
+ "allowScripts": {
77
+ "keytar@7.9.0": true,
78
+ "@azure/msal-node-extensions@5.3.5": true
79
+ }
80
+ }