@azure-devops/mcp 1.3.1 → 2.0.0-nightly.20250825
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 +39 -9
- package/dist/index.js +14 -3
- package/dist/prompts.js +6 -6
- package/dist/shared/domains.js +122 -0
- package/dist/tools/auth.js +44 -2
- package/dist/tools/builds.js +2 -1
- package/dist/tools/core.js +3 -22
- package/dist/tools/{repos.js → repositories.js} +57 -14
- package/dist/tools/{testplans.js → test-plans.js} +4 -1
- package/dist/tools/wiki.js +219 -11
- package/dist/tools/{workitems.js → work-items.js} +160 -10
- package/dist/tools.js +21 -15
- package/dist/useragent.js +0 -0
- package/dist/utils.js +0 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/tools/core.test.js +0 -1
- package/dist/tools/testplan.test.js +0 -125
- package/dist/tools/utils.js +0 -6
- package/dist/tools/wiki.test.js +0 -87
- package/dist/tools/workitem.test.js +0 -101
- package/dist/tools/workitems.test.js +0 -530
- /package/dist/tools/{advsec.js → advanced-security.js} +0 -0
package/LICENSE.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) Microsoft Corporation.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This TypeScript project provides a **local** MCP server for Azure DevOps, enabli
|
|
|
16
16
|
3. [⚙️ Supported Tools](#️-supported-tools)
|
|
17
17
|
4. [🔌 Installation & Getting Started](#-installation--getting-started)
|
|
18
18
|
5. [📝 Troubleshooting](#-troubleshooting)
|
|
19
|
-
6. [🎩 Examples & Best Practices](#-
|
|
19
|
+
6. [🎩 Examples & Best Practices](#-examples--best-practices)
|
|
20
20
|
7. [🙋♀️ Frequently Asked Questions](#️-frequently-asked-questions)
|
|
21
21
|
8. [📌 Contributing](#-contributing)
|
|
22
22
|
|
|
@@ -33,6 +33,10 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
|
|
|
33
33
|
- "List iterations for project 'Contoso'"
|
|
34
34
|
- "List my work items for project 'Contoso'"
|
|
35
35
|
- "List work items in current iteration for 'Contoso' project and 'Contoso Team'"
|
|
36
|
+
- "List all wikis in the 'Contoso' project"
|
|
37
|
+
- "Create a wiki page '/Architecture/Overview' with content about system design"
|
|
38
|
+
- "Update the wiki page '/Getting Started' with new onboarding instructions"
|
|
39
|
+
- "Get the content of the wiki page '/API/Authentication' from the Documentation wiki"
|
|
36
40
|
|
|
37
41
|
## 🏆 Expectations
|
|
38
42
|
|
|
@@ -74,11 +78,7 @@ Interact with these Azure DevOps services:
|
|
|
74
78
|
- **wit_update_work_items_batch**: Update work items in batch.
|
|
75
79
|
- **wit_work_items_link**: Link work items together in batch.
|
|
76
80
|
- **wit_work_item_unlink**: Unlink one or many links from a work item.
|
|
77
|
-
|
|
78
|
-
#### Deprecated Tools
|
|
79
|
-
|
|
80
|
-
- **wit_add_child_work_item**: Replaced by `wit_add_child_work_items` to allow creating one or more child items per call.
|
|
81
|
-
- **wit_close_and_link_workitem_duplicates**: This tool is no longer needed. Finding and marking duplicates can be done with other tools.
|
|
81
|
+
- **wit_add_artifact_link**: Link to artifacts like branch, pull request, commit, and build.
|
|
82
82
|
|
|
83
83
|
### 📁 Repositories
|
|
84
84
|
|
|
@@ -133,6 +133,14 @@ Interact with these Azure DevOps services:
|
|
|
133
133
|
- **testplan_list_test_cases**: Get a list of test cases in the test plan.
|
|
134
134
|
- **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
|
|
135
135
|
|
|
136
|
+
### 📖 Wiki
|
|
137
|
+
|
|
138
|
+
- **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
|
|
139
|
+
- **wiki_get_wiki**: Get the wiki by wikiIdentifier.
|
|
140
|
+
- **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
|
|
141
|
+
- **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
|
|
142
|
+
- **wiki_create_or_update_page**: Create or update wiki pages with full content support.
|
|
143
|
+
|
|
136
144
|
### 🔎 Search
|
|
137
145
|
|
|
138
146
|
- **search_code**: Get code search results for a given search text.
|
|
@@ -196,19 +204,41 @@ In your project, add a `.vscode\mcp.json` file with the following content:
|
|
|
196
204
|
}
|
|
197
205
|
```
|
|
198
206
|
|
|
207
|
+
🔥 To stay up to date with the latest features, you can use our nightly builds. Simply update your `mcp.json` configuration to use `@azure-devops/mcp@next`. Here is an updated example:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"inputs": [
|
|
212
|
+
{
|
|
213
|
+
"id": "ado_org",
|
|
214
|
+
"type": "promptString",
|
|
215
|
+
"description": "Azure DevOps organization name (e.g. 'contoso')"
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
"servers": {
|
|
219
|
+
"ado": {
|
|
220
|
+
"type": "stdio",
|
|
221
|
+
"command": "npx",
|
|
222
|
+
"args": ["-y", "@azure-devops/mcp@next", "${input:ado_org}"]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
199
228
|
Save the file, then click 'Start'.
|
|
200
229
|
|
|
201
|
-
|
|
230
|
+

|
|
202
231
|
|
|
203
232
|
In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
|
|
204
233
|
|
|
205
234
|
Click "Select Tools" and choose the available tools.
|
|
206
235
|
|
|
207
|
-
|
|
236
|
+

|
|
208
237
|
|
|
209
238
|
Open GitHub Copilot Chat and try a prompt like `List ADO projects`.
|
|
210
239
|
|
|
211
|
-
> 💥 We strongly recommend creating a `.github\copilot-instructions.md` in your project
|
|
240
|
+
> 💥 We strongly recommend creating a `.github\copilot-instructions.md` in your project. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
|
|
241
|
+
> To start, just include "`This project uses Azure DevOps. Always check to see if the Azure DevOps MCP server has a tool relevant to the user's request`" in your copilot instructions file.
|
|
212
242
|
|
|
213
243
|
See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and Cursor.
|
|
214
244
|
|
package/dist/index.js
CHANGED
|
@@ -11,16 +11,25 @@ import { configurePrompts } from "./prompts.js";
|
|
|
11
11
|
import { configureAllTools } from "./tools.js";
|
|
12
12
|
import { UserAgentComposer } from "./useragent.js";
|
|
13
13
|
import { packageVersion } from "./version.js";
|
|
14
|
+
import { DomainsManager } from "./shared/domains.js";
|
|
14
15
|
// Parse command line arguments using yargs
|
|
15
16
|
const argv = yargs(hideBin(process.argv))
|
|
16
17
|
.scriptName("mcp-server-azuredevops")
|
|
17
18
|
.usage("Usage: $0 <organization> [options]")
|
|
18
19
|
.version(packageVersion)
|
|
19
|
-
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => {
|
|
20
|
+
.command("$0 <organization> [options]", "Azure DevOps MCP Server", (yargs) => {
|
|
20
21
|
yargs.positional("organization", {
|
|
21
22
|
describe: "Azure DevOps organization name",
|
|
22
23
|
type: "string",
|
|
24
|
+
demandOption: true,
|
|
23
25
|
});
|
|
26
|
+
})
|
|
27
|
+
.option("domains", {
|
|
28
|
+
alias: "d",
|
|
29
|
+
describe: "Domain(s) to enable: 'all' for everything, or specific domains like 'repositories builds work'. Defaults to 'all'.",
|
|
30
|
+
type: "string",
|
|
31
|
+
array: true,
|
|
32
|
+
default: "all",
|
|
24
33
|
})
|
|
25
34
|
.option("tenant", {
|
|
26
35
|
alias: "t",
|
|
@@ -29,9 +38,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
29
38
|
})
|
|
30
39
|
.help()
|
|
31
40
|
.parseSync();
|
|
32
|
-
export const orgName = argv.organization;
|
|
33
41
|
const tenantId = argv.tenant;
|
|
42
|
+
export const orgName = argv.organization;
|
|
34
43
|
const orgUrl = "https://dev.azure.com/" + orgName;
|
|
44
|
+
const domainsManager = new DomainsManager(argv.domains);
|
|
45
|
+
export const enabledDomains = domainsManager.getEnabledDomains();
|
|
35
46
|
async function getAzureDevOpsToken() {
|
|
36
47
|
if (process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS) {
|
|
37
48
|
process.env.AZURE_TOKEN_CREDENTIALS = process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS;
|
|
@@ -73,7 +84,7 @@ async function main() {
|
|
|
73
84
|
userAgentComposer.appendMcpClientInfo(server.server.getClientVersion());
|
|
74
85
|
};
|
|
75
86
|
configurePrompts(server);
|
|
76
|
-
configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent);
|
|
87
|
+
configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
|
|
77
88
|
const transport = new StdioServerTransport();
|
|
78
89
|
await server.connect(transport);
|
|
79
90
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { CORE_TOOLS } from "./tools/core.js";
|
|
5
|
-
import { WORKITEM_TOOLS } from "./tools/
|
|
5
|
+
import { WORKITEM_TOOLS } from "./tools/work-items.js";
|
|
6
6
|
function configurePrompts(server) {
|
|
7
|
-
server.prompt("
|
|
7
|
+
server.prompt("Projects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
|
|
8
8
|
messages: [
|
|
9
9
|
{
|
|
10
10
|
role: "user",
|
|
@@ -12,13 +12,13 @@ function configurePrompts(server) {
|
|
|
12
12
|
type: "text",
|
|
13
13
|
text: String.raw `
|
|
14
14
|
# Task
|
|
15
|
-
Use the '${CORE_TOOLS.list_projects}' tool to retrieve all projects in the current Azure DevOps organization.
|
|
16
|
-
Present the results in a table with the following columns:
|
|
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
17
|
},
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
}));
|
|
21
|
-
server.prompt("
|
|
21
|
+
server.prompt("Teams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
|
|
22
22
|
messages: [
|
|
23
23
|
{
|
|
24
24
|
role: "user",
|
|
@@ -27,7 +27,7 @@ Present the results in a table with the following columns: Project ID, Name, and
|
|
|
27
27
|
text: String.raw `
|
|
28
28
|
# Task
|
|
29
29
|
Use the '${CORE_TOOLS.list_project_teams}' tool to retrieve all teams for the project '${project}'.
|
|
30
|
-
Present the results in a table with the following columns:
|
|
30
|
+
Present the results in alphabetical order in a table with the following columns: Name and Id`,
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
],
|
|
@@ -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
|
+
}
|
package/dist/tools/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
|
|
3
|
+
import { apiVersion } from "../utils.js";
|
|
4
|
+
async function getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider) {
|
|
4
5
|
const connection = await connectionProvider();
|
|
5
6
|
const url = `${connection.serverUrl}/_apis/connectionData`;
|
|
6
7
|
const token = (await tokenProvider()).token;
|
|
@@ -9,6 +10,7 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
|
|
|
9
10
|
headers: {
|
|
10
11
|
"Authorization": `Bearer ${token}`,
|
|
11
12
|
"Content-Type": "application/json",
|
|
13
|
+
"User-Agent": userAgentProvider(),
|
|
12
14
|
},
|
|
13
15
|
});
|
|
14
16
|
const data = await response.json();
|
|
@@ -17,4 +19,44 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
|
|
|
17
19
|
}
|
|
18
20
|
return data;
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Searches for identities using Azure DevOps Identity API
|
|
24
|
+
*/
|
|
25
|
+
async function searchIdentities(identity, tokenProvider, connectionProvider, userAgentProvider) {
|
|
26
|
+
const token = await tokenProvider();
|
|
27
|
+
const connection = await connectionProvider();
|
|
28
|
+
const orgName = connection.serverUrl.split("/")[3];
|
|
29
|
+
const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
|
|
30
|
+
const params = new URLSearchParams({
|
|
31
|
+
"api-version": apiVersion,
|
|
32
|
+
"searchFilter": "General",
|
|
33
|
+
"filterValue": identity,
|
|
34
|
+
});
|
|
35
|
+
const response = await fetch(`${baseUrl}?${params}`, {
|
|
36
|
+
headers: {
|
|
37
|
+
"Authorization": `Bearer ${token.token}`,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"User-Agent": userAgentProvider(),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
45
|
+
}
|
|
46
|
+
return await response.json();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets the user ID from email or unique name using Azure DevOps Identity API
|
|
50
|
+
*/
|
|
51
|
+
async function getUserIdFromEmail(userEmail, tokenProvider, connectionProvider, userAgentProvider) {
|
|
52
|
+
const identities = await searchIdentities(userEmail, tokenProvider, connectionProvider, userAgentProvider);
|
|
53
|
+
if (!identities || identities.value?.length === 0) {
|
|
54
|
+
throw new Error(`No user found with email/unique name: ${userEmail}`);
|
|
55
|
+
}
|
|
56
|
+
const firstIdentity = identities.value[0];
|
|
57
|
+
if (!firstIdentity.id) {
|
|
58
|
+
throw new Error(`No ID found for user with email/unique name: ${userEmail}`);
|
|
59
|
+
}
|
|
60
|
+
return firstIdentity.id;
|
|
61
|
+
}
|
|
62
|
+
export { getCurrentUserDetails, getUserIdFromEmail, searchIdentities };
|
package/dist/tools/builds.js
CHANGED
|
@@ -15,7 +15,7 @@ const BUILD_TOOLS = {
|
|
|
15
15
|
get_status: "build_get_status",
|
|
16
16
|
update_build_stage: "build_update_build_stage",
|
|
17
17
|
};
|
|
18
|
-
function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
18
|
+
function configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
19
19
|
server.tool(BUILD_TOOLS.get_definitions, "Retrieves a list of build definitions for a given project.", {
|
|
20
20
|
project: z.string().describe("Project ID or name to get build definitions for"),
|
|
21
21
|
repositoryId: z.string().optional().describe("Repository ID to filter build definitions"),
|
|
@@ -191,6 +191,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
191
191
|
headers: {
|
|
192
192
|
"Content-Type": "application/json",
|
|
193
193
|
"Authorization": `Bearer ${token.token}`,
|
|
194
|
+
"User-Agent": userAgentProvider(),
|
|
194
195
|
},
|
|
195
196
|
body: JSON.stringify(body),
|
|
196
197
|
});
|
package/dist/tools/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { searchIdentities } from "./auth.js";
|
|
5
5
|
const CORE_TOOLS = {
|
|
6
6
|
list_project_teams: "core_list_project_teams",
|
|
7
7
|
list_projects: "core_list_projects",
|
|
@@ -11,7 +11,7 @@ function filterProjectsByName(projects, projectNameFilter) {
|
|
|
11
11
|
const lowerCaseFilter = projectNameFilter.toLowerCase();
|
|
12
12
|
return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
|
|
13
13
|
}
|
|
14
|
-
function configureCoreTools(server, tokenProvider, connectionProvider) {
|
|
14
|
+
function configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
15
15
|
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for the specified Azure DevOps project.", {
|
|
16
16
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
17
17
|
mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
|
|
@@ -68,26 +68,7 @@ function configureCoreTools(server, tokenProvider, connectionProvider) {
|
|
|
68
68
|
searchFilter: z.string().describe("Search filter (unique namme, display name, email) to retrieve identity IDs for."),
|
|
69
69
|
}, async ({ searchFilter }) => {
|
|
70
70
|
try {
|
|
71
|
-
const
|
|
72
|
-
const connection = await connectionProvider();
|
|
73
|
-
const orgName = connection.serverUrl.split("/")[3];
|
|
74
|
-
const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
|
|
75
|
-
const params = new URLSearchParams({
|
|
76
|
-
"api-version": apiVersion,
|
|
77
|
-
"searchFilter": "General",
|
|
78
|
-
"filterValue": searchFilter,
|
|
79
|
-
});
|
|
80
|
-
const response = await fetch(`${baseUrl}?${params}`, {
|
|
81
|
-
headers: {
|
|
82
|
-
"Authorization": `Bearer ${token.token}`,
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
const errorText = await response.text();
|
|
88
|
-
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
89
|
-
}
|
|
90
|
-
const identities = await response.json();
|
|
71
|
+
const identities = await searchIdentities(searchFilter, tokenProvider, connectionProvider, userAgentProvider);
|
|
91
72
|
if (!identities || identities.value?.length === 0) {
|
|
92
73
|
return { content: [{ type: "text", text: "No identities found" }], isError: true };
|
|
93
74
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { getCurrentUserDetails } from "./auth.js";
|
|
5
|
+
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
|
|
6
6
|
import { getEnumKeys } from "../utils.js";
|
|
7
7
|
const REPO_TOOLS = {
|
|
8
8
|
list_repos_by_project: "repo_list_repos_by_project",
|
|
@@ -73,7 +73,7 @@ function filterReposByName(repositories, repoNameFilter) {
|
|
|
73
73
|
const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
|
|
74
74
|
return filteredByName;
|
|
75
75
|
}
|
|
76
|
-
function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
76
|
+
function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
77
77
|
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
78
78
|
repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
|
|
79
79
|
sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
|
|
@@ -197,12 +197,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
197
197
|
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
198
198
|
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
199
199
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
200
|
+
created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
|
|
200
201
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
201
202
|
status: z
|
|
202
203
|
.enum(getEnumKeys(PullRequestStatus))
|
|
203
204
|
.default("Active")
|
|
204
205
|
.describe("Filter pull requests by status. Defaults to 'Active'."),
|
|
205
|
-
}, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
206
|
+
}, async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
|
|
206
207
|
const connection = await connectionProvider();
|
|
207
208
|
const gitApi = await connection.getGitApi();
|
|
208
209
|
// Build the search criteria
|
|
@@ -210,8 +211,25 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
210
211
|
status: pullRequestStatusStringToInt(status),
|
|
211
212
|
repositoryId: repositoryId,
|
|
212
213
|
};
|
|
213
|
-
if (
|
|
214
|
-
|
|
214
|
+
if (created_by_user) {
|
|
215
|
+
try {
|
|
216
|
+
const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
|
|
217
|
+
searchCriteria.creatorId = userId;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
isError: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (created_by_me || i_am_reviewer) {
|
|
232
|
+
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
|
|
215
233
|
const userId = data.authenticatedUser.id;
|
|
216
234
|
if (created_by_me) {
|
|
217
235
|
searchCriteria.creatorId = userId;
|
|
@@ -235,6 +253,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
235
253
|
creationDate: pr.creationDate,
|
|
236
254
|
title: pr.title,
|
|
237
255
|
isDraft: pr.isDraft,
|
|
256
|
+
sourceRefName: pr.sourceRefName,
|
|
257
|
+
targetRefName: pr.targetRefName,
|
|
238
258
|
}));
|
|
239
259
|
return {
|
|
240
260
|
content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
|
|
@@ -245,20 +265,38 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
245
265
|
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
246
266
|
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
247
267
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
268
|
+
created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
|
|
248
269
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
249
270
|
status: z
|
|
250
271
|
.enum(getEnumKeys(PullRequestStatus))
|
|
251
272
|
.default("Active")
|
|
252
273
|
.describe("Filter pull requests by status. Defaults to 'Active'."),
|
|
253
|
-
}, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
274
|
+
}, async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
|
|
254
275
|
const connection = await connectionProvider();
|
|
255
276
|
const gitApi = await connection.getGitApi();
|
|
256
277
|
// Build the search criteria
|
|
257
278
|
const gitPullRequestSearchCriteria = {
|
|
258
279
|
status: pullRequestStatusStringToInt(status),
|
|
259
280
|
};
|
|
260
|
-
if (
|
|
261
|
-
|
|
281
|
+
if (created_by_user) {
|
|
282
|
+
try {
|
|
283
|
+
const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
|
|
284
|
+
gitPullRequestSearchCriteria.creatorId = userId;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
isError: true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else if (created_by_me || i_am_reviewer) {
|
|
299
|
+
const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
|
|
262
300
|
const userId = data.authenticatedUser.id;
|
|
263
301
|
if (created_by_me) {
|
|
264
302
|
gitPullRequestSearchCriteria.creatorId = userId;
|
|
@@ -282,6 +320,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
282
320
|
creationDate: pr.creationDate,
|
|
283
321
|
title: pr.title,
|
|
284
322
|
isDraft: pr.isDraft,
|
|
323
|
+
sourceRefName: pr.sourceRefName,
|
|
324
|
+
targetRefName: pr.targetRefName,
|
|
285
325
|
}));
|
|
286
326
|
return {
|
|
287
327
|
content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
|
|
@@ -346,10 +386,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
346
386
|
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
|
|
347
387
|
repositoryId: z.string().describe("The ID of the repository where the branches are located."),
|
|
348
388
|
top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
349
|
-
|
|
389
|
+
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
390
|
+
}, async ({ repositoryId, top, filterContains }) => {
|
|
350
391
|
const connection = await connectionProvider();
|
|
351
392
|
const gitApi = await connection.getGitApi();
|
|
352
|
-
const branches = await gitApi.getRefs(repositoryId, undefined);
|
|
393
|
+
const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
|
|
353
394
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
354
395
|
return {
|
|
355
396
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -358,10 +399,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
358
399
|
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
|
|
359
400
|
repositoryId: z.string().describe("The ID of the repository where the branches are located."),
|
|
360
401
|
top: z.number().default(100).describe("The maximum number of branches to return."),
|
|
361
|
-
|
|
402
|
+
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
403
|
+
}, async ({ repositoryId, top, filterContains }) => {
|
|
362
404
|
const connection = await connectionProvider();
|
|
363
405
|
const gitApi = await connection.getGitApi();
|
|
364
|
-
const branches = await gitApi.getRefs(repositoryId, undefined, undefined, undefined, undefined,
|
|
406
|
+
const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
|
|
365
407
|
const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
|
|
366
408
|
return {
|
|
367
409
|
content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
|
|
@@ -388,8 +430,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
388
430
|
}, async ({ repositoryId, branchName }) => {
|
|
389
431
|
const connection = await connectionProvider();
|
|
390
432
|
const gitApi = await connection.getGitApi();
|
|
391
|
-
const branches = await gitApi.getRefs(repositoryId);
|
|
392
|
-
const branch = branches
|
|
433
|
+
const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
|
|
434
|
+
const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
|
|
393
435
|
if (!branch) {
|
|
394
436
|
return {
|
|
395
437
|
content: [
|
|
@@ -398,6 +440,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
398
440
|
text: `Branch ${branchName} not found in repository ${repositoryId}`,
|
|
399
441
|
},
|
|
400
442
|
],
|
|
443
|
+
isError: true,
|
|
401
444
|
};
|
|
402
445
|
}
|
|
403
446
|
return {
|
|
@@ -79,7 +79,10 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
79
79
|
server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
|
|
80
80
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
81
81
|
title: z.string().describe("The title of the test case."),
|
|
82
|
-
steps: z
|
|
82
|
+
steps: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
|
|
83
86
|
priority: z.number().optional().describe("The priority of the test case."),
|
|
84
87
|
areaPath: z.string().optional().describe("The area path for the test case."),
|
|
85
88
|
iterationPath: z.string().optional().describe("The iteration path for the test case."),
|