@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/LICENSE.md +21 -21
- package/README.md +320 -247
- package/dist/auth.js +74 -0
- package/dist/domains.js +1 -0
- package/dist/http.js +52 -0
- package/dist/index.js +61 -28
- package/dist/org-tenants.js +73 -0
- package/dist/orgtenants.js +73 -0
- package/dist/prompts.js +35 -10
- package/dist/server.js +36 -0
- package/dist/shared/domains.js +122 -0
- package/dist/shared/tool-validation.js +92 -0
- package/dist/tenant.js +73 -0
- package/dist/tools/advanced-security.js +108 -0
- package/dist/tools/advsec.js +108 -0
- package/dist/tools/auth.js +46 -4
- package/dist/tools/builds.js +146 -21
- package/dist/tools/core.js +73 -14
- package/dist/tools/releases.js +40 -15
- package/dist/tools/repos.js +421 -54
- package/dist/tools/repositories.js +666 -0
- package/dist/tools/search.js +100 -89
- package/dist/tools/test-plans.js +213 -0
- package/dist/tools/testplans.js +22 -21
- package/dist/tools/wiki.js +295 -37
- package/dist/tools/work-items.js +809 -0
- package/dist/tools/work.js +83 -39
- package/dist/tools/workitems.js +495 -171
- package/dist/tools.js +24 -14
- package/dist/useragent.js +20 -0
- package/dist/utils.js +52 -2
- package/dist/version.js +1 -1
- package/package.json +65 -55
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 };
|
package/dist/domains.js
ADDED
|
@@ -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
|
|
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 {
|
|
13
|
+
import { UserAgentComposer } from "./useragent.js";
|
|
11
14
|
import { packageVersion } from "./version.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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 {
|
|
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("
|
|
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
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|