-
Notifications
You must be signed in to change notification settings - Fork 190
feat: add MCP server "domains" #421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a1fcd31
e59b552
5501105
636a39e
fb19526
9762dd3
e3e8216
fa431c6
3dcff4c
3f8a0fe
4facd49
b4b97d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -14,18 +14,27 @@ import { configurePrompts } from "./prompts.js"; | |||||
import { configureAllTools } from "./tools.js"; | ||||||
import { UserAgentComposer } from "./useragent.js"; | ||||||
import { packageVersion } from "./version.js"; | ||||||
import { DomainsManager } from "./shared/domains.js"; | ||||||
|
||||||
// Parse command line arguments using yargs | ||||||
const argv = yargs(hideBin(process.argv)) | ||||||
.scriptName("mcp-server-azuredevops") | ||||||
.usage("Usage: $0 <organization> [options]") | ||||||
.usage("Usage: $0 <organization> [domains...] [options]") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
.version(packageVersion) | ||||||
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => { | ||||||
.command("$0 <organization> [domains...] [options]", "Azure DevOps MCP Server", (yargs) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
yargs.positional("organization", { | ||||||
describe: "Azure DevOps organization name", | ||||||
type: "string", | ||||||
demandOption: true, | ||||||
}); | ||||||
}) | ||||||
.option("domains", { | ||||||
alias: "d", | ||||||
describe: "Domain(s) to enable: 'all' for everything, or specific domains like 'repositories builds work'. Defaults to 'all'.", | ||||||
type: "string", | ||||||
array: true, | ||||||
default: "all", | ||||||
}) | ||||||
.option("tenant", { | ||||||
alias: "t", | ||||||
describe: "Azure tenant ID (optional, required for multi-tenant scenarios)", | ||||||
|
@@ -34,10 +43,14 @@ const argv = yargs(hideBin(process.argv)) | |||||
.help() | ||||||
.parseSync(); | ||||||
|
||||||
export const orgName = argv.organization as string; | ||||||
const tenantId = argv.tenant; | ||||||
|
||||||
export const orgName = argv.organization as string; | ||||||
const orgUrl = "https://dev.azure.com/" + orgName; | ||||||
|
||||||
const domainsManager = new DomainsManager(argv.domains); | ||||||
export const enabledDomains = domainsManager.getEnabledDomains(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't find any |
||||||
|
||||||
async function getAzureDevOpsToken(): Promise<AccessToken> { | ||||||
if (process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS) { | ||||||
process.env.AZURE_TOKEN_CREDENTIALS = process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS; | ||||||
|
@@ -84,7 +97,7 @@ async function main() { | |||||
|
||||||
configurePrompts(server); | ||||||
|
||||||
configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent); | ||||||
configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent, enabledDomains); | ||||||
|
||||||
const transport = new StdioServerTransport(); | ||||||
await server.connect(transport); | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
/** | ||
* Available Azure DevOps MCP domains | ||
*/ | ||
export enum Domain { | ||
ADVANCED_SECURITY = "advanced-security", | ||
BUILDS = "builds", | ||
CORE = "core", | ||
RELEASES = "releases", | ||
REPOSITORIES = "repositories", | ||
SEARCH = "search", | ||
TEST_PLANS = "test-plans", | ||
WIKI = "wiki", | ||
WORK = "work", | ||
WORK_ITEMS = "work-items", | ||
} | ||
|
||
/** | ||
* Manages domain parsing and validation for Azure DevOps MCP server tools | ||
*/ | ||
export class DomainsManager { | ||
private static readonly AVAILABLE_DOMAINS = Object.values(Domain); | ||
|
||
private readonly enabledDomains: Set<string>; | ||
|
||
constructor(domainsInput?: string | string[]) { | ||
this.enabledDomains = new Set(); | ||
const normalizedInput = DomainsManager.parseDomainsInput(domainsInput); | ||
this.parseDomains(normalizedInput); | ||
} | ||
|
||
/** | ||
* Parse and validate domains from input | ||
* @param domainsInput - Either "all", single domain name, array of domain names, or undefined (defaults to "all") | ||
*/ | ||
private parseDomains(domainsInput?: string | string[]): void { | ||
if (!domainsInput) { | ||
console.log("No domains specified, enabling all domains for backward compatibility"); | ||
this.enableAllDomains(); | ||
return; | ||
} | ||
|
||
if (Array.isArray(domainsInput)) { | ||
this.handleArrayInput(domainsInput); | ||
return; | ||
} | ||
|
||
this.handleStringInput(domainsInput); | ||
} | ||
|
||
private handleArrayInput(domainsInput: string[]): void { | ||
if (domainsInput.length === 0) { | ||
console.log("No valid domains specified, enabling all domains by default"); | ||
this.enableAllDomains(); | ||
return; | ||
} | ||
|
||
if (domainsInput.length === 1 && domainsInput[0] === "all") { | ||
this.enableAllDomains(); | ||
return; | ||
} | ||
|
||
const domains = domainsInput.map((d) => d.trim().toLowerCase()); | ||
this.validateAndAddDomains(domains); | ||
} | ||
|
||
private handleStringInput(domainsInput: string): void { | ||
if (domainsInput === "all") { | ||
this.enableAllDomains(); | ||
return; | ||
} | ||
|
||
const domains = [domainsInput.trim().toLowerCase()]; | ||
this.validateAndAddDomains(domains); | ||
} | ||
|
||
private validateAndAddDomains(domains: string[]): void { | ||
domains.forEach((domain) => { | ||
if ((Object.values(Domain) as string[]).includes(domain)) { | ||
this.enabledDomains.add(domain); | ||
} else { | ||
console.warn(`Warning: Unknown domain '${domain}'. Available domains: ${Object.values(Domain).join(", ")}`); | ||
} | ||
}); | ||
|
||
if (this.enabledDomains.size === 0) { | ||
console.log("No valid domains specified, enabling all domains by default"); | ||
this.enableAllDomains(); | ||
} | ||
} | ||
|
||
private enableAllDomains(): void { | ||
Object.values(Domain).forEach((domain) => this.enabledDomains.add(domain)); | ||
} | ||
|
||
/** | ||
* Check if a specific domain is enabled | ||
* @param domain - Domain name to check | ||
* @returns true if domain is enabled | ||
*/ | ||
public isDomainEnabled(domain: string): boolean { | ||
return this.enabledDomains.has(domain); | ||
} | ||
|
||
/** | ||
* Get all enabled domains | ||
* @returns Set of enabled domain names | ||
*/ | ||
public getEnabledDomains(): Set<string> { | ||
return new Set(this.enabledDomains); | ||
} | ||
|
||
/** | ||
* Get list of all available domains | ||
* @returns Array of available domain names | ||
*/ | ||
public static getAvailableDomains(): string[] { | ||
return Object.values(Domain); | ||
} | ||
|
||
/** | ||
* Parse domains input from string or array to a normalized array of strings | ||
* @param domainsInput - Domains input to parse | ||
* @returns Normalized array of domain strings | ||
*/ | ||
public static parseDomainsInput(domainsInput?: string | string[]): string[] { | ||
if (!domainsInput) { | ||
return []; | ||
} | ||
|
||
if (typeof domainsInput === "string") { | ||
return domainsInput.split(",").map((d) => d.trim().toLowerCase()); | ||
} | ||
|
||
return domainsInput.map((d) => d.trim().toLowerCase()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export const packageVersion = "1.3.1"; | ||
export const packageVersion = "2.0.0"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd avoid adding an
input
for new param. It creates friction for most of the users who are not planning to use it.Instead mention it in the README, similar to
--tenant
option described in https://github.com/microsoft/azure-devops-mcp/blob/main/docs/TROUBLESHOOTING.md#solution