@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.
package/dist/auth.js ADDED
@@ -0,0 +1,74 @@
1
+ import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
2
+ import { PublicClientApplication } from "@azure/msal-node";
3
+ import open from "open";
4
+ const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
5
+ class OAuthAuthenticator {
6
+ static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
7
+ static defaultAuthority = "https://login.microsoftonline.com/common";
8
+ accountId;
9
+ publicClientApp;
10
+ constructor(tenantId) {
11
+ this.accountId = null;
12
+ this.publicClientApp = new PublicClientApplication({
13
+ auth: {
14
+ clientId: OAuthAuthenticator.clientId,
15
+ authority: tenantId ? `https://login.microsoftonline.com/${tenantId}` : OAuthAuthenticator.defaultAuthority,
16
+ },
17
+ });
18
+ }
19
+ async getToken() {
20
+ let authResult = null;
21
+ if (this.accountId) {
22
+ try {
23
+ authResult = await this.publicClientApp.acquireTokenSilent({
24
+ scopes,
25
+ account: this.accountId,
26
+ });
27
+ }
28
+ catch (error) {
29
+ authResult = null;
30
+ }
31
+ }
32
+ if (!authResult) {
33
+ authResult = await this.publicClientApp.acquireTokenInteractive({
34
+ scopes,
35
+ openBrowser: async (url) => {
36
+ open(url);
37
+ },
38
+ });
39
+ this.accountId = authResult.account;
40
+ }
41
+ if (!authResult.accessToken) {
42
+ throw new Error("Failed to obtain Azure DevOps OAuth token.");
43
+ }
44
+ return authResult.accessToken;
45
+ }
46
+ }
47
+ function createAuthenticator(type, tenantId) {
48
+ switch (type) {
49
+ case "azcli":
50
+ case "env":
51
+ if (type !== "env") {
52
+ process.env.AZURE_TOKEN_CREDENTIALS = "dev";
53
+ }
54
+ let credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
55
+ if (tenantId) {
56
+ // Use Azure CLI credential if tenantId is provided for multi-tenant scenarios
57
+ const azureCliCredential = new AzureCliCredential({ tenantId });
58
+ credential = new ChainedTokenCredential(azureCliCredential, credential);
59
+ }
60
+ return async () => {
61
+ const result = await credential.getToken(scopes);
62
+ if (!result) {
63
+ throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication.");
64
+ }
65
+ return result.token;
66
+ };
67
+ default:
68
+ const authenticator = new OAuthAuthenticator(tenantId);
69
+ return () => {
70
+ return authenticator.getToken();
71
+ };
72
+ }
73
+ }
74
+ export { createAuthenticator };
@@ -0,0 +1 @@
1
+ export {};
package/dist/http.js ADDED
@@ -0,0 +1,52 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import express from "express";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { serverBuildAndConnect } from "./server.js";
6
+ import { packageVersion } from "./version.js";
7
+ const app = express();
8
+ app.use(express.json());
9
+ app.post('/mcp/:orgName', async (req, res) => {
10
+ // In stateless mode, create a new instance of transport and server for each request
11
+ // to ensure complete isolation. A single instance would cause request ID collisions
12
+ // when multiple clients connect concurrently.
13
+ try {
14
+ const transport = new StreamableHTTPServerTransport({
15
+ sessionIdGenerator: undefined,
16
+ });
17
+ const server = await serverBuildAndConnect(req.params.orgName, transport);
18
+ res.on('close', () => {
19
+ transport.close();
20
+ server.close();
21
+ });
22
+ await transport.handleRequest(req, res, req.body);
23
+ }
24
+ catch (error) {
25
+ console.error('Error handling MCP request:', error);
26
+ if (!res.headersSent) {
27
+ res.status(500).json({
28
+ jsonrpc: '2.0',
29
+ error: {
30
+ code: -32603,
31
+ message: 'Internal server error',
32
+ },
33
+ id: null,
34
+ });
35
+ }
36
+ }
37
+ });
38
+ app.get('/mcp/:orgName', async (req, res) => {
39
+ console.log('Received GET MCP request');
40
+ res.writeHead(405).end(JSON.stringify({
41
+ jsonrpc: "2.0",
42
+ error: {
43
+ code: -32000,
44
+ message: "Method not allowed."
45
+ },
46
+ id: null
47
+ }));
48
+ });
49
+ const PORT = 3000;
50
+ app.listen(PORT, () => {
51
+ console.log(`Azure DevOps MCP Server with http transport listening on port ${PORT}. Version: ${packageVersion}`);
52
+ });
package/dist/index.js CHANGED
@@ -4,46 +4,79 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import * as azdev from "azure-devops-node-api";
7
- import { DefaultAzureCredential } from "@azure/identity";
7
+ import yargs from "yargs";
8
+ import { hideBin } from "yargs/helpers";
9
+ import { createAuthenticator } from "./auth.js";
10
+ import { getOrgTenant } from "./org-tenants.js";
8
11
  import { configurePrompts } from "./prompts.js";
9
12
  import { configureAllTools } from "./tools.js";
10
- import { userAgent } from "./utils.js";
13
+ import { UserAgentComposer } from "./useragent.js";
11
14
  import { packageVersion } from "./version.js";
12
- const args = process.argv.slice(2);
13
- if (args.length === 0) {
14
- console.error("Usage: mcp-server-azuredevops <organization_name>");
15
- process.exit(1);
16
- }
17
- export const orgName = args[0];
18
- const orgUrl = "https://dev.azure.com/" + orgName;
19
- async function getAzureDevOpsToken() {
20
- process.env.AZURE_TOKEN_CREDENTIALS = "dev";
21
- const credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
22
- const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default");
23
- return token;
24
- }
25
- async function getAzureDevOpsClient() {
26
- const token = await getAzureDevOpsToken();
27
- const authHandler = azdev.getBearerHandler(token.token);
28
- const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
29
- productName: "AzureDevOps.MCP",
30
- productVersion: packageVersion,
31
- userAgent: userAgent
15
+ import { DomainsManager } from "./shared/domains.js";
16
+ // Parse command line arguments using yargs
17
+ const argv = yargs(hideBin(process.argv))
18
+ .scriptName("mcp-server-azuredevops")
19
+ .usage("Usage: $0 <organization> [options]")
20
+ .version(packageVersion)
21
+ .command("$0 <organization> [options]", "Azure DevOps MCP Server", (yargs) => {
22
+ yargs.positional("organization", {
23
+ describe: "Azure DevOps organization name",
24
+ type: "string",
25
+ demandOption: true,
32
26
  });
33
- return connection;
27
+ })
28
+ .option("domains", {
29
+ alias: "d",
30
+ describe: "Domain(s) to enable: 'all' for everything, or specific domains like 'repositories builds work'. Defaults to 'all'.",
31
+ type: "string",
32
+ array: true,
33
+ default: "all",
34
+ })
35
+ .option("authentication", {
36
+ alias: "a",
37
+ describe: "Type of authentication to use. Supported values are 'interactive', 'azcli' and 'env' (default: 'interactive')",
38
+ type: "string",
39
+ choices: ["interactive", "azcli", "env"],
40
+ default: "interactive",
41
+ })
42
+ .option("tenant", {
43
+ alias: "t",
44
+ describe: "Azure tenant ID (optional, applied when using 'interactive' and 'azcli' type of authentication)",
45
+ type: "string",
46
+ })
47
+ .help()
48
+ .parseSync();
49
+ export const orgName = argv.organization;
50
+ const orgUrl = "https://dev.azure.com/" + orgName;
51
+ const domainsManager = new DomainsManager(argv.domains);
52
+ export const enabledDomains = domainsManager.getEnabledDomains();
53
+ function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
54
+ return async () => {
55
+ const accessToken = await getAzureDevOpsToken();
56
+ const authHandler = azdev.getBearerHandler(accessToken);
57
+ const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
58
+ productName: "AzureDevOps.MCP",
59
+ productVersion: packageVersion,
60
+ userAgent: userAgentComposer.userAgent,
61
+ });
62
+ return connection;
63
+ };
34
64
  }
35
65
  async function main() {
36
- console.error("Starting Azure DevOps MCP Server...");
37
66
  const server = new McpServer({
38
67
  name: "Azure DevOps MCP Server",
39
- version: "1.0.0",
68
+ version: packageVersion,
40
69
  });
70
+ const userAgentComposer = new UserAgentComposer(packageVersion);
71
+ server.server.oninitialized = () => {
72
+ userAgentComposer.appendMcpClientInfo(server.server.getClientVersion());
73
+ };
74
+ const tenantId = (await getOrgTenant(orgName)) ?? argv.tenant;
75
+ const authenticator = createAuthenticator(argv.authentication, tenantId);
41
76
  configurePrompts(server);
42
- configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient);
77
+ configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
43
78
  const transport = new StdioServerTransport();
44
- console.error("Connecting server to transport...");
45
79
  await server.connect(transport);
46
- console.error("Azure DevOps MCP Server running on stdio");
47
80
  }
48
81
  main().catch((error) => {
49
82
  console.error("Fatal error in main():", error);
@@ -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 trySavingCache(cache) {
17
+ try {
18
+ await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
19
+ }
20
+ catch (error) {
21
+ console.error("Failed to save org tenants 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 result
56
+ cache[orgName] = {
57
+ tenantId,
58
+ refreshedOn: Date.now(),
59
+ };
60
+ await trySavingCache(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, log and return empty result
70
+ console.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
71
+ return undefined;
72
+ }
73
+ }
@@ -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 trySavingCache(cache) {
17
+ try {
18
+ await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf-8');
19
+ }
20
+ catch (error) {
21
+ console.error('Failed to save org tenants 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 result
56
+ cache[orgName] = {
57
+ tenantId,
58
+ refreshedOn: Date.now()
59
+ };
60
+ await trySavingCache(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, log and return empty result
70
+ console.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
71
+ return undefined;
72
+ }
73
+ }
package/dist/prompts.js CHANGED
@@ -1,22 +1,47 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
- import { REPO_TOOLS } from "./tools/repos.js";
4
+ import { CORE_TOOLS } from "./tools/core.js";
5
+ import { WORKITEM_TOOLS } from "./tools/work-items.js";
5
6
  function configurePrompts(server) {
6
- server.prompt("relevant_pull_requests", "Presents the list of relevant pull requests for a given repository.", { repositoryId: z.string() }, ({ repositoryId }) => ({
7
+ server.prompt("Projects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
7
8
  messages: [
8
9
  {
9
10
  role: "user",
10
11
  content: {
11
12
  type: "text",
12
- text: String.raw `
13
- # Prerequisites
14
- 1. Unless already provided, ask user for the project name
15
- 2. Unless already provided, use '${REPO_TOOLS.list_repos_by_project}' tool to get a summarized response of the repositories in this project and ask user to select one
16
-
17
- # Task
18
- Find all pull requests for repository ${repositoryId} using '${REPO_TOOLS.list_pull_requests_by_repo}' tool and summarize them in a table.
19
- Include the following columns: ID, Title, Status, Created Date, Author and Reviewers.`,
13
+ text: String.raw `
14
+ # Task
15
+ Use the '${CORE_TOOLS.list_projects}' tool to retrieve all 'wellFormed' projects in the current Azure DevOps organization.
16
+ Present the results in alphabetical order in a table with the following columns: Name and ID.`,
17
+ },
18
+ },
19
+ ],
20
+ }));
21
+ server.prompt("Teams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
22
+ messages: [
23
+ {
24
+ role: "user",
25
+ content: {
26
+ type: "text",
27
+ text: String.raw `
28
+ # Task
29
+ Use the '${CORE_TOOLS.list_project_teams}' tool to retrieve all teams for the project '${project}'.
30
+ Present the results in alphabetical order in a table with the following columns: Name and Id`,
31
+ },
32
+ },
33
+ ],
34
+ }));
35
+ server.prompt("getWorkItem", "Retrieves details for a specific Azure DevOps work item by ID.", { id: z.string().describe("The ID of the work item to retrieve."), project: z.string().describe("The name or ID of the Azure DevOps project.") }, ({ id, project }) => ({
36
+ messages: [
37
+ {
38
+ role: "user",
39
+ content: {
40
+ type: "text",
41
+ text: String.raw `
42
+ # Task
43
+ Use the '${WORKITEM_TOOLS.get_work_item}' tool to retrieve details for the work item with ID '${id}' in project '${project}'.
44
+ Present the following fields: ID, Title, State, Assigned To, Work Item Type, Description or Repro Steps, and Created Date.`,
20
45
  },
21
46
  },
22
47
  ],
package/dist/server.js ADDED
@@ -0,0 +1,36 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import * as azdev from "azure-devops-node-api";
5
+ import { DefaultAzureCredential } from "@azure/identity";
6
+ import { configurePrompts } from "./prompts.js";
7
+ import { configureAllTools } from "./tools.js";
8
+ import { userAgent } from "./utils.js";
9
+ import { packageVersion } from "./version.js";
10
+ async function getAzureDevOpsToken() {
11
+ process.env.AZURE_TOKEN_CREDENTIALS = "dev";
12
+ const credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
13
+ const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default");
14
+ return token;
15
+ }
16
+ async function getAzureDevOpsClient(orgUrl) {
17
+ const token = await getAzureDevOpsToken();
18
+ const authHandler = azdev.getBearerHandler(token.token);
19
+ const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
20
+ productName: "AzureDevOps.MCP",
21
+ productVersion: packageVersion,
22
+ userAgent: userAgent
23
+ });
24
+ return connection;
25
+ }
26
+ export async function serverBuildAndConnect(orgName, transport) {
27
+ const server = new McpServer({
28
+ name: "Azure DevOps MCP Server",
29
+ version: packageVersion,
30
+ });
31
+ const orgUrl = "https://dev.azure.com/" + orgName;
32
+ configurePrompts(server);
33
+ configureAllTools(server, () => orgName, getAzureDevOpsToken, () => getAzureDevOpsClient(orgUrl));
34
+ await server.connect(transport);
35
+ return server;
36
+ }
@@ -0,0 +1,122 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ /**
4
+ * Available Azure DevOps MCP domains
5
+ */
6
+ export var Domain;
7
+ (function (Domain) {
8
+ Domain["ADVANCED_SECURITY"] = "advanced-security";
9
+ Domain["BUILDS"] = "builds";
10
+ Domain["CORE"] = "core";
11
+ Domain["RELEASES"] = "releases";
12
+ Domain["REPOSITORIES"] = "repositories";
13
+ Domain["SEARCH"] = "search";
14
+ Domain["TEST_PLANS"] = "test-plans";
15
+ Domain["WIKI"] = "wiki";
16
+ Domain["WORK"] = "work";
17
+ Domain["WORK_ITEMS"] = "work-items";
18
+ })(Domain || (Domain = {}));
19
+ export const ALL_DOMAINS = "all";
20
+ /**
21
+ * Manages domain parsing and validation for Azure DevOps MCP server tools
22
+ */
23
+ export class DomainsManager {
24
+ static AVAILABLE_DOMAINS = Object.values(Domain);
25
+ enabledDomains;
26
+ constructor(domainsInput) {
27
+ this.enabledDomains = new Set();
28
+ const normalizedInput = DomainsManager.parseDomainsInput(domainsInput);
29
+ this.parseDomains(normalizedInput);
30
+ }
31
+ /**
32
+ * Parse and validate domains from input
33
+ * @param domainsInput - Either "all", single domain name, array of domain names, or undefined (defaults to "all")
34
+ */
35
+ parseDomains(domainsInput) {
36
+ if (!domainsInput) {
37
+ this.enableAllDomains();
38
+ return;
39
+ }
40
+ if (Array.isArray(domainsInput)) {
41
+ this.handleArrayInput(domainsInput);
42
+ return;
43
+ }
44
+ this.handleStringInput(domainsInput);
45
+ }
46
+ handleArrayInput(domainsInput) {
47
+ if (domainsInput.length === 0 || domainsInput.includes(ALL_DOMAINS)) {
48
+ this.enableAllDomains();
49
+ return;
50
+ }
51
+ if (domainsInput.length === 1 && domainsInput[0] === ALL_DOMAINS) {
52
+ this.enableAllDomains();
53
+ return;
54
+ }
55
+ const domains = domainsInput.map((d) => d.trim().toLowerCase());
56
+ this.validateAndAddDomains(domains);
57
+ }
58
+ handleStringInput(domainsInput) {
59
+ if (domainsInput === ALL_DOMAINS) {
60
+ this.enableAllDomains();
61
+ return;
62
+ }
63
+ const domains = [domainsInput.trim().toLowerCase()];
64
+ this.validateAndAddDomains(domains);
65
+ }
66
+ validateAndAddDomains(domains) {
67
+ const availableDomainsAsStringArray = Object.values(Domain);
68
+ domains.forEach((domain) => {
69
+ if (availableDomainsAsStringArray.includes(domain)) {
70
+ this.enabledDomains.add(domain);
71
+ }
72
+ else if (domain === ALL_DOMAINS) {
73
+ this.enableAllDomains();
74
+ }
75
+ else {
76
+ console.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
77
+ }
78
+ });
79
+ if (this.enabledDomains.size === 0) {
80
+ this.enableAllDomains();
81
+ }
82
+ }
83
+ enableAllDomains() {
84
+ Object.values(Domain).forEach((domain) => this.enabledDomains.add(domain));
85
+ }
86
+ /**
87
+ * Check if a specific domain is enabled
88
+ * @param domain - Domain name to check
89
+ * @returns true if domain is enabled
90
+ */
91
+ isDomainEnabled(domain) {
92
+ return this.enabledDomains.has(domain);
93
+ }
94
+ /**
95
+ * Get all enabled domains
96
+ * @returns Set of enabled domain names
97
+ */
98
+ getEnabledDomains() {
99
+ return new Set(this.enabledDomains);
100
+ }
101
+ /**
102
+ * Get list of all available domains
103
+ * @returns Array of available domain names
104
+ */
105
+ static getAvailableDomains() {
106
+ return Object.values(Domain);
107
+ }
108
+ /**
109
+ * Parse domains input from string or array to a normalized array of strings
110
+ * @param domainsInput - Domains input to parse
111
+ * @returns Normalized array of domain strings
112
+ */
113
+ static parseDomainsInput(domainsInput) {
114
+ if (!domainsInput) {
115
+ return [];
116
+ }
117
+ if (typeof domainsInput === "string") {
118
+ return domainsInput.split(",").map((d) => d.trim().toLowerCase());
119
+ }
120
+ return domainsInput.map((d) => d.trim().toLowerCase());
121
+ }
122
+ }