@azure-devops/mcp 0.1.0 → 0.2.0-preview-oauth

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,92 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ /**
4
+ * Validates that a name conforms to Claude API requirements.
5
+ * Names must match pattern: ^[a-zA-Z0-9_.-]{1,64}$
6
+ * @param name The name to validate
7
+ * @returns Object with isValid boolean and error/reason message if invalid
8
+ */
9
+ export function validateName(name) {
10
+ // Check length
11
+ if (name.length === 0) {
12
+ return { isValid: false, error: "Name cannot be empty", reason: "name cannot be empty" };
13
+ }
14
+ if (name.length > 64) {
15
+ return {
16
+ isValid: false,
17
+ error: `Name '${name}' is ${name.length} characters long, maximum allowed is 64`,
18
+ reason: `name is ${name.length} characters long, maximum allowed is 64`,
19
+ };
20
+ }
21
+ // Check pattern: only alphanumeric, underscore, dot, and hyphen allowed
22
+ const validPattern = /^[a-zA-Z0-9_.-]+$/;
23
+ if (!validPattern.test(name)) {
24
+ return {
25
+ isValid: false,
26
+ error: `Name '${name}' contains invalid characters. Only alphanumeric characters, underscores, dots, and hyphens are allowed`,
27
+ reason: "name contains invalid characters. Only alphanumeric characters, underscores, dots, and hyphens are allowed",
28
+ };
29
+ }
30
+ return { isValid: true };
31
+ }
32
+ /**
33
+ * Validates that a tool name conforms to Claude API requirements.
34
+ * @param toolName The tool name to validate
35
+ * @returns Object with isValid boolean and error message if invalid
36
+ */
37
+ export function validateToolName(toolName) {
38
+ const result = validateName(toolName);
39
+ if (!result.isValid) {
40
+ return { isValid: false, error: result.error?.replace("Name", "Tool name") };
41
+ }
42
+ return result;
43
+ }
44
+ /**
45
+ * Validates that a parameter name conforms to Claude API requirements.
46
+ * @param paramName The parameter name to validate
47
+ * @returns Object with isValid boolean and error message if invalid
48
+ */
49
+ export function validateParameterName(paramName) {
50
+ const result = validateName(paramName);
51
+ if (!result.isValid) {
52
+ return { isValid: false, error: result.error?.replace("Name", "Parameter name") };
53
+ }
54
+ return result;
55
+ }
56
+ /**
57
+ * Extracts tool names from tool constant definitions
58
+ * @param fileContent - The content of a TypeScript file
59
+ * @returns Array of tool names found
60
+ */
61
+ export function extractToolNames(fileContent) {
62
+ const toolNames = [];
63
+ // Pattern to match tool constant definitions in tool objects
64
+ // This looks for patterns like: const SOMETHING_TOOLS = { ... } or const Test_Plan_Tools = { ... }
65
+ const toolsObjectPattern = /const\s+\w*[Tt][Oo][Oo][Ll][Ss]?\s*=\s*\{([^}]+)\}/g;
66
+ let toolsMatch;
67
+ while ((toolsMatch = toolsObjectPattern.exec(fileContent)) !== null) {
68
+ const objectContent = toolsMatch[1];
69
+ // Now extract individual tool definitions from within the object
70
+ const toolPattern = /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*"([^"]+)"/gm;
71
+ let match;
72
+ while ((match = toolPattern.exec(objectContent)) !== null) {
73
+ toolNames.push(match[1]);
74
+ }
75
+ }
76
+ return toolNames;
77
+ }
78
+ /**
79
+ * Extracts parameter names from Zod schema definitions
80
+ * @param fileContent - The content of a TypeScript file
81
+ * @returns Array of parameter names found
82
+ */
83
+ export function extractParameterNames(fileContent) {
84
+ const paramNames = [];
85
+ // Pattern to match parameter definitions like: paramName: z.string()
86
+ const paramPattern = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*z\./gm;
87
+ let match;
88
+ while ((match = paramPattern.exec(fileContent)) !== null) {
89
+ paramNames.push(match[1]);
90
+ }
91
+ return paramNames;
92
+ }
package/dist/tenant.js ADDED
@@ -0,0 +1,73 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ const CACHE_FILE = path.join(os.homedir(), '.ado_orgs.cache');
5
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 1 week in milliseconds
6
+ async function loadCache() {
7
+ try {
8
+ const cacheData = await fs.readFile(CACHE_FILE, 'utf-8');
9
+ return JSON.parse(cacheData);
10
+ }
11
+ catch (error) {
12
+ // Cache file doesn't exist or is invalid, return empty cache
13
+ return {};
14
+ }
15
+ }
16
+ async function saveCache(cache) {
17
+ try {
18
+ await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8');
19
+ }
20
+ catch (error) {
21
+ console.warn('Failed to save cache:', error);
22
+ }
23
+ }
24
+ async function fetchTenantFromApi(orgName) {
25
+ const url = `https://vssps.dev.azure.com/${orgName}`;
26
+ try {
27
+ const response = await fetch(url, { method: 'HEAD' });
28
+ if (response.status !== 404) {
29
+ throw new Error(`Expected status 404, got ${response.status}`);
30
+ }
31
+ const tenantId = response.headers.get('x-vss-resourcetenant');
32
+ if (!tenantId) {
33
+ throw new Error('x-vss-resourcetenant header not found in response');
34
+ }
35
+ return tenantId;
36
+ }
37
+ catch (error) {
38
+ throw new Error(`Failed to fetch tenant for organization ${orgName}: ${error}`);
39
+ }
40
+ }
41
+ function isCacheEntryExpired(entry) {
42
+ return Date.now() - entry.refreshedOn > CACHE_TTL_MS;
43
+ }
44
+ export async function getOrgTenant(orgName) {
45
+ // Load cache
46
+ const cache = await loadCache();
47
+ // Check if tenant is cached and not expired
48
+ const cachedEntry = cache[orgName];
49
+ if (cachedEntry && !isCacheEntryExpired(cachedEntry)) {
50
+ return cachedEntry.tenantId;
51
+ }
52
+ // Try to fetch fresh tenant from API
53
+ try {
54
+ const tenantId = await fetchTenantFromApi(orgName);
55
+ // Cache the fresh result
56
+ cache[orgName] = {
57
+ tenantId,
58
+ refreshedOn: Date.now()
59
+ };
60
+ await saveCache(cache);
61
+ return tenantId;
62
+ }
63
+ catch (error) {
64
+ // If we have an expired cache entry, return it as fallback
65
+ if (cachedEntry) {
66
+ console.error(`Failed to fetch fresh tenant for ADO org ${orgName}, using expired cache entry:`, error);
67
+ return cachedEntry.tenantId;
68
+ }
69
+ // No cache entry available, re-throw the error
70
+ console.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
71
+ return undefined;
72
+ }
73
+ }
@@ -0,0 +1,108 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { AlertType, AlertValidityStatus, Confidence, Severity, State } from "azure-devops-node-api/interfaces/AlertInterfaces.js";
4
+ import { z } from "zod";
5
+ import { getEnumKeys, mapStringArrayToEnum, mapStringToEnum } from "../utils.js";
6
+ const ADVSEC_TOOLS = {
7
+ get_alerts: "advsec_get_alerts",
8
+ get_alert_details: "advsec_get_alert_details",
9
+ };
10
+ function configureAdvSecTools(server, _, connectionProvider) {
11
+ server.tool(ADVSEC_TOOLS.get_alerts, "Retrieve Advanced Security alerts for a repository.", {
12
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
13
+ repository: z.string().describe("The name or ID of the repository to get alerts for."),
14
+ alertType: z
15
+ .enum(getEnumKeys(AlertType))
16
+ .optional()
17
+ .describe("Filter alerts by type. If not specified, returns all alert types."),
18
+ states: z
19
+ .array(z.enum(getEnumKeys(State)))
20
+ .optional()
21
+ .describe("Filter alerts by state. If not specified, returns alerts in any state."),
22
+ severities: z
23
+ .array(z.enum(getEnumKeys(Severity)))
24
+ .optional()
25
+ .describe("Filter alerts by severity level. If not specified, returns alerts at any severity."),
26
+ ruleId: z.string().optional().describe("Filter alerts by rule ID."),
27
+ ruleName: z.string().optional().describe("Filter alerts by rule name."),
28
+ toolName: z.string().optional().describe("Filter alerts by tool name."),
29
+ ref: z.string().optional().describe("Filter alerts by git reference (branch). If not provided and onlyDefaultBranch is true, only includes alerts from default branch."),
30
+ onlyDefaultBranch: z.boolean().optional().default(true).describe("If true, only return alerts found on the default branch. Defaults to true."),
31
+ confidenceLevels: z
32
+ .array(z.enum(getEnumKeys(Confidence)))
33
+ .optional()
34
+ .default(["high", "other"])
35
+ .describe("Filter alerts by confidence levels. Only applicable for secret alerts. Defaults to both 'high' and 'other'."),
36
+ validity: z
37
+ .array(z.enum(getEnumKeys(AlertValidityStatus)))
38
+ .optional()
39
+ .describe("Filter alerts by validity status. Only applicable for secret alerts."),
40
+ top: z.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
41
+ orderBy: z.enum(["id", "firstSeen", "lastSeen", "fixedOn", "severity"]).optional().default("severity").describe("Order results by specified field. Defaults to 'severity'."),
42
+ continuationToken: z.string().optional().describe("Continuation token for pagination."),
43
+ }, async ({ project, repository, alertType, states, severities, ruleId, ruleName, toolName, ref, onlyDefaultBranch, confidenceLevels, validity, top, orderBy, continuationToken }) => {
44
+ try {
45
+ const connection = await connectionProvider();
46
+ const alertApi = await connection.getAlertApi();
47
+ const isSecretAlert = !alertType || alertType.toLowerCase() === "secret";
48
+ const criteria = {
49
+ ...(alertType && { alertType: mapStringToEnum(alertType, AlertType) }),
50
+ ...(states && { states: mapStringArrayToEnum(states, State) }),
51
+ ...(severities && { severities: mapStringArrayToEnum(severities, Severity) }),
52
+ ...(ruleId && { ruleId }),
53
+ ...(ruleName && { ruleName }),
54
+ ...(toolName && { toolName }),
55
+ ...(ref && { ref }),
56
+ ...(onlyDefaultBranch !== undefined && { onlyDefaultBranch }),
57
+ ...(isSecretAlert && confidenceLevels && { confidenceLevels: mapStringArrayToEnum(confidenceLevels, Confidence) }),
58
+ ...(isSecretAlert && validity && { validity: mapStringArrayToEnum(validity, AlertValidityStatus) }),
59
+ };
60
+ const result = await alertApi.getAlerts(project, repository, top, orderBy, criteria, undefined, // expand parameter
61
+ continuationToken);
62
+ return {
63
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
64
+ };
65
+ }
66
+ catch (error) {
67
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Error fetching Advanced Security alerts: ${errorMessage}`,
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ });
79
+ server.tool(ADVSEC_TOOLS.get_alert_details, "Get detailed information about a specific Advanced Security alert.", {
80
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
81
+ repository: z.string().describe("The name or ID of the repository containing the alert."),
82
+ alertId: z.number().describe("The ID of the alert to retrieve details for."),
83
+ ref: z.string().optional().describe("Git reference (branch) to filter the alert."),
84
+ }, async ({ project, repository, alertId, ref }) => {
85
+ try {
86
+ const connection = await connectionProvider();
87
+ const alertApi = await connection.getAlertApi();
88
+ const result = await alertApi.getAlert(project, alertId, repository, ref, undefined // expand parameter
89
+ );
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
92
+ };
93
+ }
94
+ catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: `Error fetching alert details: ${errorMessage}`,
101
+ },
102
+ ],
103
+ isError: true,
104
+ };
105
+ }
106
+ });
107
+ }
108
+ export { ADVSEC_TOOLS, configureAdvSecTools };
@@ -0,0 +1,108 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { AlertType, AlertValidityStatus, Confidence, Severity, State } from "azure-devops-node-api/interfaces/AlertInterfaces.js";
4
+ import { z } from "zod";
5
+ import { getEnumKeys, mapStringArrayToEnum, mapStringToEnum } from "../utils.js";
6
+ const ADVSEC_TOOLS = {
7
+ get_alerts: "advsec_get_alerts",
8
+ get_alert_details: "advsec_get_alert_details",
9
+ };
10
+ function configureAdvSecTools(server, tokenProvider, connectionProvider) {
11
+ server.tool(ADVSEC_TOOLS.get_alerts, "Retrieve Advanced Security alerts for a repository.", {
12
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
13
+ repository: z.string().describe("The name or ID of the repository to get alerts for."),
14
+ alertType: z
15
+ .enum(getEnumKeys(AlertType))
16
+ .optional()
17
+ .describe("Filter alerts by type. If not specified, returns all alert types."),
18
+ states: z
19
+ .array(z.enum(getEnumKeys(State)))
20
+ .optional()
21
+ .describe("Filter alerts by state. If not specified, returns alerts in any state."),
22
+ severities: z
23
+ .array(z.enum(getEnumKeys(Severity)))
24
+ .optional()
25
+ .describe("Filter alerts by severity level. If not specified, returns alerts at any severity."),
26
+ ruleId: z.string().optional().describe("Filter alerts by rule ID."),
27
+ ruleName: z.string().optional().describe("Filter alerts by rule name."),
28
+ toolName: z.string().optional().describe("Filter alerts by tool name."),
29
+ ref: z.string().optional().describe("Filter alerts by git reference (branch). If not provided and onlyDefaultBranch is true, only includes alerts from default branch."),
30
+ onlyDefaultBranch: z.boolean().optional().default(true).describe("If true, only return alerts found on the default branch. Defaults to true."),
31
+ confidenceLevels: z
32
+ .array(z.enum(getEnumKeys(Confidence)))
33
+ .optional()
34
+ .default(["high", "other"])
35
+ .describe("Filter alerts by confidence levels. Only applicable for secret alerts. Defaults to both 'high' and 'other'."),
36
+ validity: z
37
+ .array(z.enum(getEnumKeys(AlertValidityStatus)))
38
+ .optional()
39
+ .describe("Filter alerts by validity status. Only applicable for secret alerts."),
40
+ top: z.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
41
+ orderBy: z.enum(["id", "firstSeen", "lastSeen", "fixedOn", "severity"]).optional().default("severity").describe("Order results by specified field. Defaults to 'severity'."),
42
+ continuationToken: z.string().optional().describe("Continuation token for pagination."),
43
+ }, async ({ project, repository, alertType, states, severities, ruleId, ruleName, toolName, ref, onlyDefaultBranch, confidenceLevels, validity, top, orderBy, continuationToken }) => {
44
+ try {
45
+ const connection = await connectionProvider();
46
+ const alertApi = await connection.getAlertApi();
47
+ const isSecretAlert = !alertType || alertType.toLowerCase() === "secret";
48
+ const criteria = {
49
+ ...(alertType && { alertType: mapStringToEnum(alertType, AlertType) }),
50
+ ...(states && { states: mapStringArrayToEnum(states, State) }),
51
+ ...(severities && { severities: mapStringArrayToEnum(severities, Severity) }),
52
+ ...(ruleId && { ruleId }),
53
+ ...(ruleName && { ruleName }),
54
+ ...(toolName && { toolName }),
55
+ ...(ref && { ref }),
56
+ ...(onlyDefaultBranch !== undefined && { onlyDefaultBranch }),
57
+ ...(isSecretAlert && confidenceLevels && { confidenceLevels: mapStringArrayToEnum(confidenceLevels, Confidence) }),
58
+ ...(isSecretAlert && validity && { validity: mapStringArrayToEnum(validity, AlertValidityStatus) }),
59
+ };
60
+ const result = await alertApi.getAlerts(project, repository, top, orderBy, criteria, undefined, // expand parameter
61
+ continuationToken);
62
+ return {
63
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
64
+ };
65
+ }
66
+ catch (error) {
67
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Error fetching Advanced Security alerts: ${errorMessage}`,
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ });
79
+ server.tool(ADVSEC_TOOLS.get_alert_details, "Get detailed information about a specific Advanced Security alert.", {
80
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
81
+ repository: z.string().describe("The name or ID of the repository containing the alert."),
82
+ alertId: z.number().describe("The ID of the alert to retrieve details for."),
83
+ ref: z.string().optional().describe("Git reference (branch) to filter the alert."),
84
+ }, async ({ project, repository, alertId, ref }) => {
85
+ try {
86
+ const connection = await connectionProvider();
87
+ const alertApi = await connection.getAlertApi();
88
+ const result = await alertApi.getAlert(project, alertId, repository, ref, undefined // expand parameter
89
+ );
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
92
+ };
93
+ }
94
+ catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: `Error fetching alert details: ${errorMessage}`,
101
+ },
102
+ ],
103
+ isError: true,
104
+ };
105
+ }
106
+ });
107
+ }
108
+ export { ADVSEC_TOOLS, configureAdvSecTools };
@@ -1,14 +1,16 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- async function getCurrentUserDetails(tokenProvider, connectionProvider) {
3
+ import { apiVersion } from "../utils.js";
4
+ async function getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider) {
4
5
  const connection = await connectionProvider();
5
6
  const url = `${connection.serverUrl}/_apis/connectionData`;
6
- const token = (await tokenProvider()).token;
7
+ const token = await tokenProvider();
7
8
  const response = await fetch(url, {
8
9
  method: "GET",
9
10
  headers: {
10
- Authorization: `Bearer ${token}`,
11
+ "Authorization": `Bearer ${token}`,
11
12
  "Content-Type": "application/json",
13
+ "User-Agent": userAgentProvider(),
12
14
  },
13
15
  });
14
16
  const data = await response.json();
@@ -17,4 +19,44 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
17
19
  }
18
20
  return data;
19
21
  }
20
- export { getCurrentUserDetails };
22
+ /**
23
+ * Searches for identities using Azure DevOps Identity API
24
+ */
25
+ async function searchIdentities(identity, tokenProvider, connectionProvider, userAgentProvider) {
26
+ const token = await tokenProvider();
27
+ const connection = await connectionProvider();
28
+ const orgName = connection.serverUrl.split("/")[3];
29
+ const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
30
+ const params = new URLSearchParams({
31
+ "api-version": apiVersion,
32
+ "searchFilter": "General",
33
+ "filterValue": identity,
34
+ });
35
+ const response = await fetch(`${baseUrl}?${params}`, {
36
+ headers: {
37
+ "Authorization": `Bearer ${token}`,
38
+ "Content-Type": "application/json",
39
+ "User-Agent": userAgentProvider(),
40
+ },
41
+ });
42
+ if (!response.ok) {
43
+ const errorText = await response.text();
44
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+ /**
49
+ * Gets the user ID from email or unique name using Azure DevOps Identity API
50
+ */
51
+ async function getUserIdFromEmail(userEmail, tokenProvider, connectionProvider, userAgentProvider) {
52
+ const identities = await searchIdentities(userEmail, tokenProvider, connectionProvider, userAgentProvider);
53
+ if (!identities || identities.value?.length === 0) {
54
+ throw new Error(`No user found with email/unique name: ${userEmail}`);
55
+ }
56
+ const firstIdentity = identities.value[0];
57
+ if (!firstIdentity.id) {
58
+ throw new Error(`No ID found for user with email/unique name: ${userEmail}`);
59
+ }
60
+ return firstIdentity.id;
61
+ }
62
+ export { getCurrentUserDetails, getUserIdFromEmail, searchIdentities };