Skip to content

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
{
"id": "ado_org",
"type": "promptString",
"description": "Azure DevOps organization name (e.g. 'contoso')"
"description": "Azure DevOps organization name (e.g. 'contoso')"
},
{
"id": "domains",
"type": "promptString",
"description": "Enter one or more Azure DevOps domains separated by commas. Available options: advanced-security, builds, core, releases, repositories, search, test-plans, wiki, work, work-items, all. (e.g. 'core,work-items,builds' or 'all')",
"default": "all"
}
],
"servers": {
"ado": {
"type": "stdio",
"command": "mcp-server-azuredevops",
"args": ["${input:ado_org}"]
"args": ["${input:ado_org}", "${input:domains}"]
}
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure-devops/mcp",
"version": "1.3.1",
"version": "2.0.0",
"description": "MCP server for interacting with Azure DevOps",
"license": "MIT",
"author": "Microsoft Corporation",
Expand Down
Empty file added src/domains.ts
Empty file.
30 changes: 22 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +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]")
.version(packageVersion)
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => {
yargs.positional("organization", {
describe: "Azure DevOps organization name",
type: "string",
});
.command("$0 <organization> [domains...] [options]", "Azure DevOps MCP Server", (yargs) => {
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",
Expand All @@ -34,10 +44,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();

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;
Expand Down Expand Up @@ -84,7 +98,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);
Expand Down
2 changes: 1 addition & 1 deletion src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { CORE_TOOLS } from "./tools/core.js";
import { WORKITEM_TOOLS } from "./tools/workitems.js";
import { WORKITEM_TOOLS } from "./tools/work-items.js";

function configurePrompts(server: McpServer) {
server.prompt("listProjects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
Expand Down
139 changes: 139 additions & 0 deletions src/shared/domains.ts
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());
}
}
37 changes: 22 additions & 15 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,35 @@ import { AccessToken } from "@azure/identity";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebApi } from "azure-devops-node-api";

import { configureAdvSecTools } from "./tools/advsec.js";
import { Domain } from "./shared/domains.js";
import { configureAdvSecTools } from "./tools/advanced-security.js";
import { configureBuildTools } from "./tools/builds.js";
import { configureCoreTools } from "./tools/core.js";
import { configureReleaseTools } from "./tools/releases.js";
import { configureRepoTools } from "./tools/repos.js";
import { configureRepoTools } from "./tools/repositories.js";
import { configureSearchTools } from "./tools/search.js";
import { configureTestPlanTools } from "./tools/testplans.js";
import { configureTestPlanTools } from "./tools/test-plans.js";
import { configureWikiTools } from "./tools/wiki.js";
import { configureWorkTools } from "./tools/work.js";
import { configureWorkItemTools } from "./tools/workitems.js";
import { configureWorkItemTools } from "./tools/work-items.js";

function configureAllTools(server: McpServer, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string) {
configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider);
configureWorkTools(server, tokenProvider, connectionProvider);
configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider);
configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider);
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
configureReleaseTools(server, tokenProvider, connectionProvider);
configureWikiTools(server, tokenProvider, connectionProvider);
configureTestPlanTools(server, tokenProvider, connectionProvider);
configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider);
configureAdvSecTools(server, tokenProvider, connectionProvider);
function configureAllTools(server: McpServer, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string, enabledDomains: Set<string>) {
const configureIfDomainEnabled = (domain: string, configureFn: () => void) => {
if (enabledDomains.has(domain)) {
configureFn();
}
};

configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
configureIfDomainEnabled(Domain.BUILDS, () => configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider));
configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
configureIfDomainEnabled(Domain.RELEASES, () => configureReleaseTools(server, tokenProvider, connectionProvider));
configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider));
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider));
configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
}

export { configureAllTools };
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const packageVersion = "1.3.1";
export const packageVersion = "2.0.0";
Loading
Loading