Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a1fcd31
feat: add domains input and loading logic
Novaes Aug 10, 2025
e59b552
test: add tools.test.ts to test domains load logic
Novaes Aug 11, 2025
5501105
refactor: update files to match domain names
Novaes Aug 11, 2025
636a39e
refactor: rename target_domains to domains
Novaes Aug 11, 2025
fb19526
Merge branch 'main' of github.com:microsoft/azure-devops-mcp into use…
Novaes Aug 14, 2025
9762dd3
chore: remove tools.test.ts
Novaes Aug 14, 2025
e3e8216
2.0.0
Novaes Aug 14, 2025
fa431c6
fix: removes pickManyString not support
Novaes Aug 14, 2025
3dcff4c
test: fix test
Novaes Aug 17, 2025
3f8a0fe
test: removes tools.test.ts
Novaes Aug 17, 2025
4facd49
Merge branch 'main' into users/mnovaes/add-domains
Novaes Aug 17, 2025
b4b97d2
chore: format
Novaes Aug 18, 2025
a0ac933
Update src/index.ts
Novaes Aug 22, 2025
b643eaa
Update src/index.ts
Novaes Aug 22, 2025
36dc38c
chore: remove domains from default mcp.json file
Novaes Aug 22, 2025
cecfa76
Merge branch 'main' of github.com:microsoft/azure-devops-mcp into use…
Novaes Aug 22, 2025
37c29b5
Merge branch 'users/mnovaes/add-domains' of github.com:microsoft/azur…
Novaes Aug 22, 2025
6504ff7
chore: add domains to usage
Novaes Aug 22, 2025
fe0dc1f
fix: removes unused file
Novaes Aug 22, 2025
c0e0a53
chore: add logging
Novaes Aug 22, 2025
5974a96
chore: extra logging and update intTest mcp.json
Novaes Aug 22, 2025
6e1080e
fix: format / mcp.json
Novaes Aug 22, 2025
c9df143
fix: revert package.json
Novaes Aug 22, 2025
e55894f
Merge branch 'main' of github.com:microsoft/azure-devops-mcp into use…
Novaes Aug 25, 2025
9e4de14
chore: update domains spec mcp.json for integration test
Novaes Aug 25, 2025
6993c73
chore: use binary alias
Novaes Aug 25, 2025
a05a44d
chore: update comment on intTest/domains/mcp.json
Novaes Aug 25, 2025
4be8e63
fix: removes console.log due conflict on stdio on Claude
Novaes Aug 25, 2025
2a8b99f
chore: revert positional args
Novaes Aug 25, 2025
ea58092
chore: remove logs from tools.ts
Novaes Aug 25, 2025
dba8569
test: update tests for console chnges
Novaes Aug 25, 2025
87a0ae0
test: update stderr log message
Novaes Aug 25, 2025
acc94b5
misc: npm audit fix
Novaes Aug 25, 2025
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
23 changes: 23 additions & 0 deletions intTest/domains/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"servers": {
"ado": {
"type": "stdio",
"command": "mcp-server-azuredevops",
"args": [
"${input:ado_org}",
"-d",
"core",
"work",
"workitems"
// ... any other domain to enable, you can also use 'all' (which is already the default)
]
}
},
"inputs": [
{
"id": "ado_org",
"type": "promptString",
"description": "Azure DevOps organization name (e.g. 'contoso')"
}
]
}
16 changes: 8 additions & 8 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
19 changes: 16 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
.version(packageVersion)
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => {
.command("$0 <organization> [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",
describe: "Azure tenant ID (optional, required for multi-tenant scenarios)",
Expand All @@ -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();

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 +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);
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("Projects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
Expand Down
141 changes: 141 additions & 0 deletions src/shared/domains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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",
}

export const ALL_DOMAINS = "all";

/**
* 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) {
this.enableAllDomains();
return;
}

if (Array.isArray(domainsInput)) {
this.handleArrayInput(domainsInput);
return;
}

this.handleStringInput(domainsInput);
}

private handleArrayInput(domainsInput: string[]): void {
if (domainsInput.length === 0 || domainsInput.includes(ALL_DOMAINS)) {
this.enableAllDomains();
return;
}

if (domainsInput.length === 1 && domainsInput[0] === ALL_DOMAINS) {
this.enableAllDomains();
return;
}

const domains = domainsInput.map((d) => d.trim().toLowerCase());
this.validateAndAddDomains(domains);
}

private handleStringInput(domainsInput: string): void {
if (domainsInput === ALL_DOMAINS) {
this.enableAllDomains();
return;
}

const domains = [domainsInput.trim().toLowerCase()];
this.validateAndAddDomains(domains);
}

private validateAndAddDomains(domains: string[]): void {
const availableDomainsAsStringArray = Object.values(Domain) as string[];
domains.forEach((domain) => {
if (availableDomainsAsStringArray.includes(domain)) {
this.enabledDomains.add(domain);
} else if (domain === ALL_DOMAINS) {
this.enableAllDomains();
} else {
console.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
}
});

if (this.enabledDomains.size === 0) {
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