-
Notifications
You must be signed in to change notification settings - Fork 211
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
Changes from 27 commits
a1fcd31
e59b552
5501105
636a39e
fb19526
9762dd3
e3e8216
fa431c6
3dcff4c
3f8a0fe
4facd49
b4b97d2
a0ac933
b643eaa
36dc38c
cecfa76
37c29b5
6504ff7
fe0dc1f
c0e0a53
5974a96
6e1080e
c9df143
e55894f
9e4de14
6993c73
a05a44d
4be8e63
2a8b99f
ea58092
dba8569
87a0ae0
acc94b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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')" | ||
} | ||
] | ||
} |
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...] [more options]") | ||
Novaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.version(packageVersion) | ||
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => { | ||
.command("$0 <organization> [domains...] [more 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", | ||
}) | ||
Novaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.option("tenant", { | ||
alias: "t", | ||
describe: "Azure tenant ID (optional, required for multi-tenant scenarios)", | ||
|
@@ -34,10 +43,15 @@ 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(); | ||
Novaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
console.log(`Enabled domains: ${Array.from(enabledDomains).join(", ")}`); | ||
|
||
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; | ||
|
@@ -72,6 +86,13 @@ function getAzureDevOpsClient(userAgentComposer: UserAgentComposer): () => Promi | |
} | ||
|
||
async function main() { | ||
console.log("Starting Azure DevOps MCP Server..."); | ||
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. Claude MCP client is strict. logging to STDOUT is not an option. Please remove everywhere 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. 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. At some point, we should implement persistent logging. Logs are essential for tracing errors and evaluating application security by analyzing flow execution. These logs can be stored in a file for proper retention and review. |
||
console.log("Enabled domains:", Array.from(enabledDomains)); | ||
console.log("Parsed arguments:", { | ||
organization: argv.organization, | ||
domains: argv.domains, | ||
tenant: argv.tenant, | ||
}); | ||
const server = new McpServer({ | ||
name: "Azure DevOps MCP Server", | ||
version: packageVersion, | ||
|
@@ -84,7 +105,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,140 @@ | ||
// 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", | ||
} | ||
Novaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* 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") { | ||
Novaes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) { | ||
console.log(`Adding domain: ${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"; |
Uh oh!
There was an error while loading. Please reload this page.