@azure-devops/mcp 1.2.0-daily.20250715 β 1.2.0
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/README.md +165 -101
- package/dist/index.js +32 -8
- package/dist/tools/builds.js +14 -7
- package/dist/tools/releases.js +26 -7
- package/dist/tools/repos.js +16 -9
- package/dist/tools/search.js +85 -80
- package/dist/tools/workitems.js +108 -41
- package/dist/utils.js +26 -0
- package/dist/version.js +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -5,20 +5,20 @@ Easily install the Azure DevOps MCP Server for VS Code or VS Code Insiders:
|
|
|
5
5
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
6
6
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&quality=insiders&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
7
7
|
|
|
8
|
-
This TypeScript project
|
|
8
|
+
This TypeScript project provides a **local** MCP server for Azure DevOps, enabling you to perform a wide range of Azure DevOps tasks directly from your code editor.
|
|
9
9
|
|
|
10
|
-
> π¨ **Public Preview:** This project is in public preview.
|
|
10
|
+
> π¨ **Public Preview:** This project is in public preview. Tools and features may change before general availability.
|
|
11
11
|
|
|
12
|
-
## π Table of
|
|
12
|
+
## π Table of Contents
|
|
13
13
|
|
|
14
14
|
1. [πΊ Overview](#-overview)
|
|
15
15
|
2. [π Expectations](#-expectations)
|
|
16
|
-
3. [βοΈ Supported
|
|
17
|
-
4. [π Installation &
|
|
16
|
+
3. [βοΈ Supported Tools](#οΈ-supported-tools)
|
|
17
|
+
4. [π Installation & Getting Started](#-installation--getting-started)
|
|
18
18
|
5. [π¦ Usage](#-usage)
|
|
19
19
|
6. [π Troubleshooting](#-troubleshooting)
|
|
20
|
-
7. [π© Samples &
|
|
21
|
-
8. [πββοΈ Frequently
|
|
20
|
+
7. [π© Samples & Best Practices](#-samples--best-practices)
|
|
21
|
+
8. [πββοΈ Frequently Asked Questions](#οΈ-frequently-asked-questions)
|
|
22
22
|
9. [π Contributing](#-contributing)
|
|
23
23
|
|
|
24
24
|
## πΊ Overview
|
|
@@ -37,9 +37,9 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
|
|
|
37
37
|
|
|
38
38
|
## π Expectations
|
|
39
39
|
|
|
40
|
-
The Azure DevOps MCP Server is built from tools that are concise, simple, focused, and easy to use
|
|
40
|
+
The Azure DevOps MCP Server is built from tools that are concise, simple, focused, and easy to useβeach designed for a specific scenario. We intentionally avoid complex tools that try to do too much. The goal is to provide a thin abstraction layer over the REST APIs, making data access straightforward and letting the language model handle complex reasoning.
|
|
41
41
|
|
|
42
|
-
## βοΈ Supported
|
|
42
|
+
## βοΈ Supported Tools
|
|
43
43
|
|
|
44
44
|
Interact with these Azure DevOps services:
|
|
45
45
|
|
|
@@ -61,32 +61,32 @@ Interact with these Azure DevOps services:
|
|
|
61
61
|
- **wit_list_backlogs**: Retrieve a list of backlogs for a given project and team.
|
|
62
62
|
- **wit_list_backlog_work_items**: Retrieve a list of backlogs for a given project, team, and backlog category.
|
|
63
63
|
- **wit_get_work_item**: Get a single work item by ID.
|
|
64
|
-
- **wit_get_work_items_batch_by_ids**:
|
|
64
|
+
- **wit_get_work_items_batch_by_ids**: Retrieve a list of work items by IDs in batch.
|
|
65
65
|
- **wit_update_work_item**: Update a work item by ID with specified fields.
|
|
66
66
|
- **wit_create_work_item**: Create a new work item in a specified project and work item type.
|
|
67
|
-
- **wit_list_work_item_comments**:
|
|
68
|
-
- **wit_get_work_items_for_iteration**:
|
|
69
|
-
- **wit_add_work_item_comment**: Add comment to a work item by ID.
|
|
70
|
-
- **wit_add_child_work_items**: Create one or
|
|
67
|
+
- **wit_list_work_item_comments**: Retrieve a list of comments for a work item by ID.
|
|
68
|
+
- **wit_get_work_items_for_iteration**: Retrieve a list of work items for a specified iteration.
|
|
69
|
+
- **wit_add_work_item_comment**: Add a comment to a work item by ID.
|
|
70
|
+
- **wit_add_child_work_items**: Create one or more child work items of a specific work item type for the given parent ID.
|
|
71
71
|
- **wit_link_work_item_to_pull_request**: Link a single work item to an existing pull request.
|
|
72
72
|
- **wit_get_work_item_type**: Get a specific work item type.
|
|
73
73
|
- **wit_get_query**: Get a query by its ID or path.
|
|
74
74
|
- **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
|
|
75
75
|
- **wit_update_work_items_batch**: Update work items in batch.
|
|
76
|
-
- **wit_close_and_link_workitem_duplicates**: Close duplicate work items by
|
|
76
|
+
- **wit_close_and_link_workitem_duplicates**: Close duplicate work items by ID.
|
|
77
77
|
- **wit_work_items_link**: Link work items together in batch.
|
|
78
78
|
|
|
79
|
-
#### Deprecated
|
|
79
|
+
#### Deprecated Tools
|
|
80
80
|
|
|
81
|
-
- **wit_add_child_work_item**: Replaced by `wit_add_child_work_items`
|
|
81
|
+
- **wit_add_child_work_item**: Replaced by `wit_add_child_work_items` to allow creating one or more child items per call.
|
|
82
82
|
|
|
83
83
|
### π Repositories
|
|
84
84
|
|
|
85
85
|
- **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
|
|
86
86
|
- **repo_list_pull_requests_by_repo**: Retrieve a list of pull requests for a given repository.
|
|
87
|
-
- **repo_list_pull_requests_by_project**: Retrieve a list of pull requests for a given project
|
|
87
|
+
- **repo_list_pull_requests_by_project**: Retrieve a list of pull requests for a given project ID or name.
|
|
88
88
|
- **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
|
|
89
|
-
- **repo_list_my_branches_by_repo**: Retrieve a list of
|
|
89
|
+
- **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
|
|
90
90
|
- **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
|
|
91
91
|
- **repo_list_pull_request_threads**: Retrieve a list of comment threads for a pull request.
|
|
92
92
|
- **repo_list_pull_request_thread_comments**: Retrieve a list of comments in a pull request thread.
|
|
@@ -94,49 +94,52 @@ Interact with these Azure DevOps services:
|
|
|
94
94
|
- **repo_get_branch_by_name**: Get a branch by its name.
|
|
95
95
|
- **repo_get_pull_request_by_id**: Get a pull request by its ID.
|
|
96
96
|
- **repo_create_pull_request**: Create a new pull request.
|
|
97
|
-
- **repo_update_pull_request_status**: Update status of an existing pull request to active or abandoned.
|
|
97
|
+
- **repo_update_pull_request_status**: Update the status of an existing pull request to active or abandoned.
|
|
98
98
|
- **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
|
|
99
|
-
- **repo_reply_to_comment**:
|
|
100
|
-
- **repo_resolve_comment**:
|
|
101
|
-
- **repo_search_commits**:
|
|
99
|
+
- **repo_reply_to_comment**: Reply to a specific comment on a pull request.
|
|
100
|
+
- **repo_resolve_comment**: Resolve a specific comment thread on a pull request.
|
|
101
|
+
- **repo_search_commits**: Search for commits.
|
|
102
102
|
|
|
103
103
|
### π°οΈ Builds
|
|
104
104
|
|
|
105
|
-
- **build_get_definitions**:
|
|
106
|
-
- **build_get_definition_revisions**:
|
|
107
|
-
- **build_get_builds**:
|
|
108
|
-
- **build_get_log**:
|
|
105
|
+
- **build_get_definitions**: Retrieve a list of build definitions for a given project.
|
|
106
|
+
- **build_get_definition_revisions**: Retrieve a list of revisions for a specific build definition.
|
|
107
|
+
- **build_get_builds**: Retrieve a list of builds for a given project.
|
|
108
|
+
- **build_get_log**: Retrieve the logs for a specific build.
|
|
109
109
|
- **build_get_log_by_id**: Get a specific build log by log ID.
|
|
110
110
|
- **build_get_changes**: Get the changes associated with a specific build.
|
|
111
|
-
- **build_run_build**:
|
|
112
|
-
- **build_get_status**:
|
|
113
|
-
- **build_update_build_stage**:
|
|
111
|
+
- **build_run_build**: Trigger a new build for a specified definition.
|
|
112
|
+
- **build_get_status**: Fetch the status of a specific build.
|
|
113
|
+
- **build_update_build_stage**: Update the stage of a specific build.
|
|
114
114
|
|
|
115
115
|
### π Releases
|
|
116
116
|
|
|
117
|
-
- **release_get_definitions**:
|
|
118
|
-
- **release_get_releases**:
|
|
117
|
+
- **release_get_definitions**: Retrieve a list of release definitions for a given project.
|
|
118
|
+
- **release_get_releases**: Retrieve a list of releases for a given project.
|
|
119
119
|
|
|
120
120
|
### π§ͺ Test Plans
|
|
121
121
|
|
|
122
|
-
- **testplan_create_test_plan**:
|
|
123
|
-
- **testplan_create_test_case**:
|
|
124
|
-
- **testplan_add_test_cases_to_suite**:
|
|
122
|
+
- **testplan_create_test_plan**: Create a new test plan in the project.
|
|
123
|
+
- **testplan_create_test_case**: Create a new test case work item.
|
|
124
|
+
- **testplan_add_test_cases_to_suite**: Add existing test cases to a test suite.
|
|
125
125
|
- **testplan_list_test_plans**: Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.
|
|
126
|
-
- **testplan_list_test_cases**:
|
|
127
|
-
- **testplan_show_test_results_from_build_id**:
|
|
126
|
+
- **testplan_list_test_cases**: Get a list of test cases in the test plan.
|
|
127
|
+
- **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
|
|
128
128
|
|
|
129
129
|
### π Search
|
|
130
130
|
|
|
131
|
-
- **search_code**: Get
|
|
131
|
+
- **search_code**: Get code search results for a given search text.
|
|
132
132
|
- **search_wiki**: Get wiki search results for a given search text.
|
|
133
133
|
- **search_workitem**: Get work item search results for a given search text.
|
|
134
134
|
|
|
135
|
-
## π Installation &
|
|
135
|
+
## π Installation & Getting Started
|
|
136
136
|
|
|
137
137
|
Clone the repository, install dependencies, and add it to your MCP client configuration.
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
[VS Code and GitHub Copilot](#%EF%B8%8F-visual-studio-code--github-copilot)<br/>
|
|
140
|
+
[Visual Studio 2022 and GitHub Copilot](#%EF%B8%8F-visual-studio-2022--github-copilot)
|
|
141
|
+
|
|
142
|
+
### β‘οΈ Visual Studio Code & GitHub Copilot
|
|
140
143
|
|
|
141
144
|
For the best experience, use Visual Studio Code and GitHub Copilot.
|
|
142
145
|
|
|
@@ -157,51 +160,51 @@ az login
|
|
|
157
160
|
|
|
158
161
|
### Installation
|
|
159
162
|
|
|
160
|
-
#### β¨ One-Click
|
|
163
|
+
#### β¨ One-Click Install
|
|
161
164
|
|
|
162
165
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
163
166
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&quality=insiders&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
164
167
|
|
|
165
168
|
After installation, select GitHub Copilot Agent Mode and refresh the tools list. Learn more about Agent Mode in the [VS Code Documentation](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode).
|
|
166
169
|
|
|
167
|
-
#### π§¨
|
|
170
|
+
#### 𧨠Install from Public Feed (Recommended)
|
|
168
171
|
|
|
169
|
-
This installation method is the easiest for all users
|
|
172
|
+
This installation method is the easiest for all users of Visual Studio Code.
|
|
170
173
|
|
|
171
174
|
π₯ [Watch this quick start video to get up and running in under two minutes!](https://youtu.be/EUmFM6qXoYk)
|
|
172
175
|
|
|
173
176
|
##### Steps
|
|
174
177
|
|
|
175
|
-
1. In your project, add a `.vscode\mcp.json` file
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
2. Save the file, then click 'Start
|
|
197
|
-
|
|
198
|
-
|
|
178
|
+
1. In your project, add a `.vscode\mcp.json` file with the following content:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"inputs": [
|
|
183
|
+
{
|
|
184
|
+
"id": "ado_org",
|
|
185
|
+
"type": "promptString",
|
|
186
|
+
"description": "Azure DevOps organization name (e.g. 'contoso')"
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"servers": {
|
|
190
|
+
"ado": {
|
|
191
|
+
"type": "stdio",
|
|
192
|
+
"command": "npx",
|
|
193
|
+
"args": ["-y", "@azure-devops/mcp", "${input:ado_org}"]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
2. Save the file, then click 'Start'.
|
|
200
|
+
|
|
201
|
+
<img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
|
|
199
202
|
|
|
200
203
|
3. In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
|
|
201
204
|
4. Click "Select Tools" and choose the available tools.
|
|
202
|
-
5. We strongly recommend
|
|
205
|
+
5. We strongly recommend creating a `.github\copilot-instructions.md` in your project and copying the contents from this [copilot-instructions.md](./.github/copilot-instructions.md) file. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
|
|
203
206
|
|
|
204
|
-
#### π οΈ
|
|
207
|
+
#### π οΈ Install from Source (Dev Mode)
|
|
205
208
|
|
|
206
209
|
This installation method is recommended for advanced users and contributors who want immediate access to the latest updates from the main branch. It is ideal if you are developing new tools, enhancing existing features, or maintaining a custom fork.
|
|
207
210
|
|
|
@@ -211,39 +214,100 @@ This installation method is recommended for advanced users and contributors who
|
|
|
211
214
|
|
|
212
215
|
1. Clone the repository.
|
|
213
216
|
2. Install dependencies:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npm install
|
|
220
|
+
```
|
|
221
|
+
|
|
217
222
|
3. Edit or add `.vscode/mcp.json`:
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
4. Start the Azure DevOps MCP Server
|
|
239
|
-
|
|
240
|
-
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"inputs": [
|
|
227
|
+
{
|
|
228
|
+
"id": "ado_org",
|
|
229
|
+
"type": "promptString",
|
|
230
|
+
"description": "Azure DevOps organization's name (e.g. 'contoso')"
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
"servers": {
|
|
234
|
+
"ado": {
|
|
235
|
+
"type": "stdio",
|
|
236
|
+
"command": "mcp-server-azuredevops",
|
|
237
|
+
"args": ["${input:ado_org}"]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
4. Start the Azure DevOps MCP Server.
|
|
244
|
+
|
|
245
|
+
<img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
|
|
241
246
|
|
|
242
247
|
5. In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
|
|
243
248
|
6. Click "Select Tools" and choose the available tools.
|
|
244
|
-
7. We strongly recommend
|
|
249
|
+
7. We strongly recommend creating a `.github\copilot-instructions.md` in your project and copying the contents from this [copilot-instructions.md](./.github/copilot-instructions.md) file. This will help you get the best experience using the Azure DevOps MCP Server in GitHub Copilot Chat.
|
|
250
|
+
|
|
251
|
+
See the [How To](./docs/HOWTO.md) section for details.
|
|
252
|
+
|
|
253
|
+
### β‘οΈ Visual Studio 2022 & GitHub Copilot
|
|
254
|
+
|
|
255
|
+
For the best experience, use Visual Studio Code and GitHub Copilot π.
|
|
256
|
+
|
|
257
|
+
### Prerequisites
|
|
258
|
+
|
|
259
|
+
1. Install [VS Studio 2022 version 17.14](https://learn.microsoft.com/en-us/visualstudio/releases/2022/release-history) or later
|
|
260
|
+
2. Install [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)
|
|
261
|
+
3. Open a project in Visual Studio.
|
|
262
|
+
|
|
263
|
+
### Azure Login
|
|
264
|
+
|
|
265
|
+
Ensure you are logged in to Azure DevOps via the Azure CLI:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
az login
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### 𧨠Install from Public Feed (Recommended)
|
|
272
|
+
|
|
273
|
+
This installation method is the easiest for all users of Visual Studio 2022.
|
|
274
|
+
|
|
275
|
+
π₯ [Watch this quick start video to get up and running in under two minutes!](https://youtu.be/nz_Gn-WL7j0)
|
|
276
|
+
|
|
277
|
+
##### Steps
|
|
278
|
+
|
|
279
|
+
1. Add a `.mcp.json` file to the solution folder with the following content:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"inputs": [
|
|
284
|
+
{
|
|
285
|
+
"id": "ado_org",
|
|
286
|
+
"type": "promptString",
|
|
287
|
+
"description": "Azure DevOps organization name (e.g. 'contoso')"
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
"servers": {
|
|
291
|
+
"ado": {
|
|
292
|
+
"type": "stdio",
|
|
293
|
+
"command": "npx",
|
|
294
|
+
"args": ["-y", "@azure-devops/mcp", "${input:ado_org}"]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
2. Save the file.
|
|
301
|
+
3. Add your organization name by clicking on the `input` option.
|
|
302
|
+
|
|
303
|
+
<img src="./docs/media/start-mcp-server-from-vs.png" alt="start mcp server from visual studio 2022" width="250"/>
|
|
304
|
+
|
|
305
|
+
4. Open Copilot chat and switch to [Agent Mode](https://learn.microsoft.com/en-us/visualstudio/ide/copilot-agent-mode?view=vs-2022).
|
|
306
|
+
5. Click the "Tools" icon and choose the available tools.
|
|
307
|
+
|
|
308
|
+
<img src="./docs/media/set-tools-from-vs.png" alt="set tools to use in visual studio 2022" width="250"/>
|
|
245
309
|
|
|
246
|
-
|
|
310
|
+
6. We strongly recommend creating a `.github\copilot-instructions.md` in your project and copying the contents from this [copilot-instructions.md](./.github/copilot-instructions.md) file. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
|
|
247
311
|
|
|
248
312
|
## π¦ Usage
|
|
249
313
|
|
|
@@ -263,23 +327,23 @@ See [How To](./docs/HOWTO.md) section for details
|
|
|
263
327
|
3. Select desired `ado` tools.
|
|
264
328
|
4. Try prompts like "List ADO projects".
|
|
265
329
|
|
|
266
|
-
For more details, see [Visual Studio MCP Servers documentation](https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers?view=vs-2022) and [Getting Started Video](https://www.youtube.com/watch?v=oPFecZHBCkg).
|
|
330
|
+
For more details, see [Visual Studio MCP Servers documentation](https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers?view=vs-2022) and the [Getting Started Video](https://www.youtube.com/watch?v=oPFecZHBCkg).
|
|
267
331
|
|
|
268
332
|
## π Troubleshooting
|
|
269
333
|
|
|
270
334
|
See the [Troubleshooting guide](./docs/TROUBLESHOOTING.md) for help with common issues and logging.
|
|
271
335
|
|
|
272
|
-
## π© Samples &
|
|
336
|
+
## π© Samples & Best Practices
|
|
273
337
|
|
|
274
338
|
Find sample prompts and best practices in our [How-to Guide](./docs/HOWTO.md).
|
|
275
339
|
|
|
276
|
-
## πββοΈ Frequently
|
|
340
|
+
## πββοΈ Frequently Asked Questions
|
|
277
341
|
|
|
278
342
|
For answers to common questions about the Azure DevOps MCP Server, see the [Frequently Asked Questions](./docs/FAQ.md).
|
|
279
343
|
|
|
280
344
|
## π Contributing
|
|
281
345
|
|
|
282
|
-
We welcome contributions! During preview, please file
|
|
346
|
+
We welcome contributions! During preview, please file issues for bugs, enhancements, or documentation improvements.
|
|
283
347
|
|
|
284
348
|
See our [Contributions Guide](./CONTRIBUTING.md) for:
|
|
285
349
|
|
|
@@ -288,7 +352,7 @@ See our [Contributions Guide](./CONTRIBUTING.md) for:
|
|
|
288
352
|
- π Code style & testing
|
|
289
353
|
- π Pull request process
|
|
290
354
|
|
|
291
|
-
## π€ Code of
|
|
355
|
+
## π€ Code of Conduct
|
|
292
356
|
|
|
293
357
|
This project follows the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
294
358
|
For questions, see the [FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [open@microsoft.com](mailto:open@microsoft.com).
|
package/dist/index.js
CHANGED
|
@@ -4,17 +4,33 @@
|
|
|
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 { DefaultAzureCredential } from "@azure/identity";
|
|
7
|
+
import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
|
|
8
|
+
import yargs from "yargs";
|
|
9
|
+
import { hideBin } from "yargs/helpers";
|
|
8
10
|
import { configurePrompts } from "./prompts.js";
|
|
9
11
|
import { configureAllTools } from "./tools.js";
|
|
10
12
|
import { UserAgentComposer } from "./useragent.js";
|
|
11
13
|
import { packageVersion } from "./version.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// Parse command line arguments using yargs
|
|
15
|
+
const argv = yargs(hideBin(process.argv))
|
|
16
|
+
.scriptName("mcp-server-azuredevops")
|
|
17
|
+
.usage("Usage: $0 <organization> [options]")
|
|
18
|
+
.version(packageVersion)
|
|
19
|
+
.command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => {
|
|
20
|
+
yargs.positional("organization", {
|
|
21
|
+
describe: "Azure DevOps organization name",
|
|
22
|
+
type: "string",
|
|
23
|
+
});
|
|
24
|
+
})
|
|
25
|
+
.option("tenant", {
|
|
26
|
+
alias: "t",
|
|
27
|
+
describe: "Azure tenant ID (optional, required for multi-tenant scenarios)",
|
|
28
|
+
type: "string",
|
|
29
|
+
})
|
|
30
|
+
.help()
|
|
31
|
+
.parseSync();
|
|
32
|
+
export const orgName = argv.organization;
|
|
33
|
+
const tenantId = argv.tenant;
|
|
18
34
|
const orgUrl = "https://dev.azure.com/" + orgName;
|
|
19
35
|
async function getAzureDevOpsToken() {
|
|
20
36
|
if (process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS) {
|
|
@@ -23,8 +39,16 @@ async function getAzureDevOpsToken() {
|
|
|
23
39
|
else {
|
|
24
40
|
process.env.AZURE_TOKEN_CREDENTIALS = "dev";
|
|
25
41
|
}
|
|
26
|
-
|
|
42
|
+
let credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
|
|
43
|
+
if (tenantId) {
|
|
44
|
+
// Use Azure CLI credential if tenantId is provided for multi-tenant scenarios
|
|
45
|
+
const azureCliCredential = new AzureCliCredential({ tenantId });
|
|
46
|
+
credential = new ChainedTokenCredential(azureCliCredential, credential);
|
|
47
|
+
}
|
|
27
48
|
const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default");
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged in or another token source setup correctly.");
|
|
51
|
+
}
|
|
28
52
|
return token;
|
|
29
53
|
}
|
|
30
54
|
function getAzureDevOpsClient(userAgentComposer) {
|
package/dist/tools/builds.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import { apiVersion } from "../utils.js";
|
|
3
|
+
import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
4
4
|
import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
@@ -22,7 +22,10 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
22
22
|
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter build definitions"),
|
|
23
23
|
name: z.string().optional().describe("Name of the build definition to filter"),
|
|
24
24
|
path: z.string().optional().describe("Path of the build definition to filter"),
|
|
25
|
-
queryOrder: z
|
|
25
|
+
queryOrder: z
|
|
26
|
+
.enum(getEnumKeys(DefinitionQueryOrder))
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Order in which build definitions are returned"),
|
|
26
29
|
top: z.number().optional().describe("Maximum number of build definitions to return"),
|
|
27
30
|
continuationToken: z.string().optional().describe("Token for continuing paged results"),
|
|
28
31
|
minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter build definitions"),
|
|
@@ -37,7 +40,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
37
40
|
}, async ({ project, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
|
|
38
41
|
const connection = await connectionProvider();
|
|
39
42
|
const buildApi = await connection.getBuildApi();
|
|
40
|
-
const buildDefinitions = await buildApi.getDefinitions(project, name, repositoryId, repositoryType, queryOrder, top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
|
|
43
|
+
const buildDefinitions = await buildApi.getDefinitions(project, name, repositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
|
|
41
44
|
return {
|
|
42
45
|
content: [{ type: "text", text: JSON.stringify(buildDefinitions, null, 2) }],
|
|
43
46
|
};
|
|
@@ -70,7 +73,11 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
70
73
|
continuationToken: z.string().optional().describe("Token for continuing paged results"),
|
|
71
74
|
maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition"),
|
|
72
75
|
deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum)"),
|
|
73
|
-
queryOrder: z
|
|
76
|
+
queryOrder: z
|
|
77
|
+
.enum(getEnumKeys(BuildQueryOrder))
|
|
78
|
+
.default("QueueTimeDescending")
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Order in which builds are returned"),
|
|
74
81
|
branchName: z.string().optional().describe("Branch name to filter builds"),
|
|
75
82
|
buildIds: z.array(z.number()).optional().describe("Array of build IDs to retrieve"),
|
|
76
83
|
repositoryId: z.string().optional().describe("Repository ID to filter builds"),
|
|
@@ -78,7 +85,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
78
85
|
}, async ({ project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType, }) => {
|
|
79
86
|
const connection = await connectionProvider();
|
|
80
87
|
const buildApi = await connection.getBuildApi();
|
|
81
|
-
const builds = await buildApi.getBuilds(project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType);
|
|
88
|
+
const builds = await buildApi.getBuilds(project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, safeEnumConvert(BuildQueryOrder, queryOrder), branchName, buildIds, repositoryId, repositoryType);
|
|
82
89
|
return {
|
|
83
90
|
content: [{ type: "text", text: JSON.stringify(builds, null, 2) }],
|
|
84
91
|
};
|
|
@@ -168,7 +175,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
168
175
|
project: z.string().describe("Project ID or name to update the build stage for"),
|
|
169
176
|
buildId: z.number().describe("ID of the build to update"),
|
|
170
177
|
stageName: z.string().describe("Name of the stage to update"),
|
|
171
|
-
status: z.
|
|
178
|
+
status: z.enum(getEnumKeys(StageUpdateType)).describe("New status for the stage"),
|
|
172
179
|
forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage."),
|
|
173
180
|
}, async ({ project, buildId, stageName, status, forceRetryAllJobs }) => {
|
|
174
181
|
const connection = await connectionProvider();
|
|
@@ -177,7 +184,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
|
|
|
177
184
|
const token = await tokenProvider();
|
|
178
185
|
const body = {
|
|
179
186
|
forceRetryAllJobs: forceRetryAllJobs,
|
|
180
|
-
state: status
|
|
187
|
+
state: safeEnumConvert(StageUpdateType, status),
|
|
181
188
|
};
|
|
182
189
|
const response = await fetch(endpoint, {
|
|
183
190
|
method: "PATCH",
|
package/dist/tools/releases.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { ReleaseDefinitionExpands, ReleaseDefinitionQueryOrder, ReleaseExpands, ReleaseStatus, ReleaseQueryOrder } from "azure-devops-node-api/interfaces/ReleaseInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
5
6
|
const RELEASE_TOOLS = {
|
|
6
7
|
get_release_definitions: "release_get_definitions",
|
|
7
8
|
get_releases: "release_get_releases",
|
|
@@ -10,12 +11,18 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
|
10
11
|
server.tool(RELEASE_TOOLS.get_release_definitions, "Retrieves list of release definitions for a given project.", {
|
|
11
12
|
project: z.string().describe("Project ID or name to get release definitions for"),
|
|
12
13
|
searchText: z.string().optional().describe("Search text to filter release definitions"),
|
|
13
|
-
expand: z
|
|
14
|
+
expand: z
|
|
15
|
+
.enum(getEnumKeys(ReleaseDefinitionExpands))
|
|
16
|
+
.default("None")
|
|
17
|
+
.describe("Expand options for release definitions"),
|
|
14
18
|
artifactType: z.string().optional().describe("Filter by artifact type"),
|
|
15
19
|
artifactSourceId: z.string().optional().describe("Filter by artifact source ID"),
|
|
16
20
|
top: z.number().optional().describe("Number of results to return (for pagination)"),
|
|
17
21
|
continuationToken: z.string().optional().describe("Continuation token for pagination"),
|
|
18
|
-
queryOrder: z
|
|
22
|
+
queryOrder: z
|
|
23
|
+
.enum(getEnumKeys(ReleaseDefinitionQueryOrder))
|
|
24
|
+
.default("NameAscending")
|
|
25
|
+
.describe("Order of the results"),
|
|
19
26
|
path: z.string().optional().describe("Path to filter release definitions"),
|
|
20
27
|
isExactNameMatch: z.boolean().optional().default(false).describe("Whether to match the exact name of the release definition. Default is false."),
|
|
21
28
|
tagFilter: z.array(z.string()).optional().describe("Filter by tags associated with the release definitions"),
|
|
@@ -26,7 +33,7 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
|
26
33
|
}, async ({ project, searchText, expand, artifactType, artifactSourceId, top, continuationToken, queryOrder, path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName, }) => {
|
|
27
34
|
const connection = await connectionProvider();
|
|
28
35
|
const releaseApi = await connection.getReleaseApi();
|
|
29
|
-
const releaseDefinitions = await releaseApi.getReleaseDefinitions(project, searchText, expand, artifactType, artifactSourceId, top, continuationToken, queryOrder, path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName);
|
|
36
|
+
const releaseDefinitions = await releaseApi.getReleaseDefinitions(project, searchText, safeEnumConvert(ReleaseDefinitionExpands, expand), artifactType, artifactSourceId, top, continuationToken, safeEnumConvert(ReleaseDefinitionQueryOrder, queryOrder), path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName);
|
|
30
37
|
return {
|
|
31
38
|
content: [{ type: "text", text: JSON.stringify(releaseDefinitions, null, 2) }],
|
|
32
39
|
};
|
|
@@ -37,7 +44,11 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
|
37
44
|
definitionEnvironmentId: z.number().optional().describe("ID of the definition environment to filter releases"),
|
|
38
45
|
searchText: z.string().optional().describe("Search text to filter releases"),
|
|
39
46
|
createdBy: z.string().optional().describe("User ID or name who created the release"),
|
|
40
|
-
statusFilter: z
|
|
47
|
+
statusFilter: z
|
|
48
|
+
.enum(getEnumKeys(ReleaseStatus))
|
|
49
|
+
.optional()
|
|
50
|
+
.default("Active")
|
|
51
|
+
.describe("Status of the releases to filter (default: Active)"),
|
|
41
52
|
environmentStatusFilter: z.number().optional().describe("Environment status to filter releases"),
|
|
42
53
|
minCreatedTime: z.coerce
|
|
43
54
|
.date()
|
|
@@ -53,10 +64,18 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
|
53
64
|
.optional()
|
|
54
65
|
.default(() => new Date())
|
|
55
66
|
.describe("Maximum created time for releases (default: now)"),
|
|
56
|
-
queryOrder: z
|
|
67
|
+
queryOrder: z
|
|
68
|
+
.enum(getEnumKeys(ReleaseQueryOrder))
|
|
69
|
+
.optional()
|
|
70
|
+
.default("Ascending")
|
|
71
|
+
.describe("Order in which to return releases (default: Ascending)"),
|
|
57
72
|
top: z.number().optional().describe("Number of releases to return"),
|
|
58
73
|
continuationToken: z.number().optional().describe("Continuation token for pagination"),
|
|
59
|
-
expand: z
|
|
74
|
+
expand: z
|
|
75
|
+
.enum(getEnumKeys(ReleaseExpands))
|
|
76
|
+
.optional()
|
|
77
|
+
.default("None")
|
|
78
|
+
.describe("Expand options for releases"),
|
|
60
79
|
artifactTypeId: z.string().optional().describe("Filter releases by artifact type ID"),
|
|
61
80
|
sourceId: z.string().optional().describe("Filter releases by artifact source ID"),
|
|
62
81
|
artifactVersionId: z.string().optional().describe("Filter releases by artifact version ID"),
|
|
@@ -69,7 +88,7 @@ function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
|
69
88
|
}, async ({ project, definitionId, definitionEnvironmentId, searchText, createdBy, statusFilter, environmentStatusFilter, minCreatedTime, maxCreatedTime, queryOrder, top, continuationToken, expand, artifactTypeId, sourceId, artifactVersionId, sourceBranchFilter, isDeleted, tagFilter, propertyFilters, releaseIdFilter, path, }) => {
|
|
70
89
|
const connection = await connectionProvider();
|
|
71
90
|
const releaseApi = await connection.getReleaseApi();
|
|
72
|
-
const releases = await releaseApi.getReleases(project, definitionId, definitionEnvironmentId, searchText, createdBy, statusFilter, environmentStatusFilter, minCreatedTime, maxCreatedTime, queryOrder, top, continuationToken, expand, artifactTypeId, sourceId, artifactVersionId, sourceBranchFilter, isDeleted, tagFilter, propertyFilters, releaseIdFilter, path);
|
|
91
|
+
const releases = await releaseApi.getReleases(project, definitionId, definitionEnvironmentId, searchText, createdBy, safeEnumConvert(ReleaseStatus, statusFilter), environmentStatusFilter, minCreatedTime, maxCreatedTime, safeEnumConvert(ReleaseQueryOrder, queryOrder), top, continuationToken, safeEnumConvert(ReleaseExpands, expand), artifactTypeId, sourceId, artifactVersionId, sourceBranchFilter, isDeleted, tagFilter, propertyFilters, releaseIdFilter, path);
|
|
73
92
|
return {
|
|
74
93
|
content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
|
|
75
94
|
};
|
package/dist/tools/repos.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getCurrentUserDetails } from "./auth.js";
|
|
6
|
+
import { getEnumKeys } from "../utils.js";
|
|
6
7
|
const REPO_TOOLS = {
|
|
7
8
|
list_repos_by_project: "repo_list_repos_by_project",
|
|
8
9
|
list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
|
|
@@ -32,15 +33,15 @@ function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
|
32
33
|
}
|
|
33
34
|
function pullRequestStatusStringToInt(status) {
|
|
34
35
|
switch (status) {
|
|
35
|
-
case "
|
|
36
|
+
case "Abandoned":
|
|
36
37
|
return PullRequestStatus.Abandoned.valueOf();
|
|
37
|
-
case "
|
|
38
|
+
case "Active":
|
|
38
39
|
return PullRequestStatus.Active.valueOf();
|
|
39
|
-
case "
|
|
40
|
+
case "All":
|
|
40
41
|
return PullRequestStatus.All.valueOf();
|
|
41
|
-
case "
|
|
42
|
+
case "Completed":
|
|
42
43
|
return PullRequestStatus.Completed.valueOf();
|
|
43
|
-
case "
|
|
44
|
+
case "NotSet":
|
|
44
45
|
return PullRequestStatus.NotSet.valueOf();
|
|
45
46
|
default:
|
|
46
47
|
throw new Error(`Unknown pull request status: ${status}`);
|
|
@@ -79,11 +80,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
79
80
|
server.tool(REPO_TOOLS.update_pull_request_status, "Update status of an existing pull request to active or abandoned.", {
|
|
80
81
|
repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
|
|
81
82
|
pullRequestId: z.number().describe("The ID of the pull request to be published."),
|
|
82
|
-
status: z.enum(["
|
|
83
|
+
status: z.enum(["Active", "Abandoned"]).describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
|
|
83
84
|
}, async ({ repositoryId, pullRequestId, status }) => {
|
|
84
85
|
const connection = await connectionProvider();
|
|
85
86
|
const gitApi = await connection.getGitApi();
|
|
86
|
-
const statusValue = status === "
|
|
87
|
+
const statusValue = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
|
|
87
88
|
const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
|
|
88
89
|
return {
|
|
89
90
|
content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
|
|
@@ -144,7 +145,10 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
144
145
|
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
145
146
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
146
147
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
147
|
-
status: z
|
|
148
|
+
status: z
|
|
149
|
+
.enum(getEnumKeys(PullRequestStatus))
|
|
150
|
+
.default("Active")
|
|
151
|
+
.describe("Filter pull requests by status. Defaults to 'Active'."),
|
|
148
152
|
}, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
149
153
|
const connection = await connectionProvider();
|
|
150
154
|
const gitApi = await connection.getGitApi();
|
|
@@ -189,7 +193,10 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
|
|
|
189
193
|
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
190
194
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
191
195
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
192
|
-
status: z
|
|
196
|
+
status: z
|
|
197
|
+
.enum(getEnumKeys(PullRequestStatus))
|
|
198
|
+
.default("Active")
|
|
199
|
+
.describe("Filter pull requests by status. Defaults to 'Active'."),
|
|
193
200
|
}, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
|
|
194
201
|
const connection = await connectionProvider();
|
|
195
202
|
const gitApi = await connection.getGitApi();
|
package/dist/tools/search.js
CHANGED
|
@@ -10,38 +10,37 @@ const SEARCH_TOOLS = {
|
|
|
10
10
|
search_workitem: "search_workitem",
|
|
11
11
|
};
|
|
12
12
|
function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
filters: z
|
|
24
|
-
.object({
|
|
25
|
-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
26
|
-
Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
|
|
27
|
-
Path: z.array(z.string()).optional().describe("Filter in these paths"),
|
|
28
|
-
Branch: z.array(z.string()).optional().describe("Filter in these branches"),
|
|
29
|
-
CodeElement: z.array(z.string()).optional().describe("Filter for these code elements (e.g., classes, functions, symbols)"),
|
|
30
|
-
// Note: CodeElement is optional and can be used to filter results by specific code elements.
|
|
31
|
-
// It can be a string or an array of strings.
|
|
32
|
-
// If provided, the search will only return results that match the specified code elements.
|
|
33
|
-
// This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
|
|
34
|
-
// Example: CodeElement: ["MyClass", "MyFunction"]
|
|
35
|
-
})
|
|
36
|
-
.partial()
|
|
37
|
-
.optional(),
|
|
38
|
-
includeFacets: z.boolean().optional(),
|
|
39
|
-
})
|
|
40
|
-
.strict(),
|
|
41
|
-
}, async ({ searchRequest }) => {
|
|
13
|
+
server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
|
|
14
|
+
searchText: z.string().describe("Keywords to search for in code repositories"),
|
|
15
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
16
|
+
repository: z.array(z.string()).optional().describe("Filter by repositories"),
|
|
17
|
+
path: z.array(z.string()).optional().describe("Filter by paths"),
|
|
18
|
+
branch: z.array(z.string()).optional().describe("Filter by branches"),
|
|
19
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
20
|
+
$skip: z.number().default(0).describe("Number of results to skip"),
|
|
21
|
+
$top: z.number().default(5).describe("Maximum number of results to return"),
|
|
22
|
+
}, async ({ searchText, project, repository, path, branch, includeFacets, $skip, $top }) => {
|
|
42
23
|
const accessToken = await tokenProvider();
|
|
43
24
|
const connection = await connectionProvider();
|
|
44
25
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
|
|
26
|
+
const requestBody = {
|
|
27
|
+
searchText,
|
|
28
|
+
includeFacets,
|
|
29
|
+
$skip,
|
|
30
|
+
$top,
|
|
31
|
+
};
|
|
32
|
+
const filters = {};
|
|
33
|
+
if (project && project.length > 0)
|
|
34
|
+
filters.Project = project;
|
|
35
|
+
if (repository && repository.length > 0)
|
|
36
|
+
filters.Repository = repository;
|
|
37
|
+
if (path && path.length > 0)
|
|
38
|
+
filters.Path = path;
|
|
39
|
+
if (branch && branch.length > 0)
|
|
40
|
+
filters.Branch = branch;
|
|
41
|
+
if (Object.keys(filters).length > 0) {
|
|
42
|
+
requestBody.filters = filters;
|
|
43
|
+
}
|
|
45
44
|
const response = await fetch(url, {
|
|
46
45
|
method: "POST",
|
|
47
46
|
headers: {
|
|
@@ -49,44 +48,43 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
49
48
|
"Authorization": `Bearer ${accessToken.token}`,
|
|
50
49
|
"User-Agent": userAgentProvider(),
|
|
51
50
|
},
|
|
52
|
-
body: JSON.stringify(
|
|
51
|
+
body: JSON.stringify(requestBody),
|
|
53
52
|
});
|
|
54
53
|
if (!response.ok) {
|
|
55
54
|
throw new Error(`Azure DevOps Code Search API error: ${response.status} ${response.statusText}`);
|
|
56
55
|
}
|
|
57
56
|
const resultText = await response.text();
|
|
58
57
|
const resultJson = JSON.parse(resultText);
|
|
59
|
-
const topResults = Array.isArray(resultJson.results) ? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length)) : [];
|
|
60
58
|
const gitApi = await connection.getGitApi();
|
|
61
|
-
const combinedResults = await fetchCombinedResults(
|
|
59
|
+
const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
|
|
62
60
|
return {
|
|
63
61
|
content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
|
|
64
62
|
};
|
|
65
63
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
|
|
75
|
-
$top: z.number().default(10).describe("Number of results to return (for pagination)"),
|
|
76
|
-
filters: z
|
|
77
|
-
.object({
|
|
78
|
-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
79
|
-
Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
|
|
80
|
-
})
|
|
81
|
-
.partial()
|
|
82
|
-
.optional()
|
|
83
|
-
.describe("Filters to apply to the search text"),
|
|
84
|
-
includeFacets: z.boolean().optional(),
|
|
85
|
-
})
|
|
86
|
-
.strict(),
|
|
87
|
-
}, async ({ searchRequest }) => {
|
|
64
|
+
server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
|
|
65
|
+
searchText: z.string().describe("Keywords to search for wiki pages"),
|
|
66
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
67
|
+
wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
|
|
68
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
69
|
+
$skip: z.number().default(0).describe("Number of results to skip"),
|
|
70
|
+
$top: z.number().default(10).describe("Maximum number of results to return"),
|
|
71
|
+
}, async ({ searchText, project, wiki, includeFacets, $skip, $top }) => {
|
|
88
72
|
const accessToken = await tokenProvider();
|
|
89
73
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
74
|
+
const requestBody = {
|
|
75
|
+
searchText,
|
|
76
|
+
includeFacets,
|
|
77
|
+
$skip,
|
|
78
|
+
$top,
|
|
79
|
+
};
|
|
80
|
+
const filters = {};
|
|
81
|
+
if (project && project.length > 0)
|
|
82
|
+
filters.Project = project;
|
|
83
|
+
if (wiki && wiki.length > 0)
|
|
84
|
+
filters.Wiki = wiki;
|
|
85
|
+
if (Object.keys(filters).length > 0) {
|
|
86
|
+
requestBody.filters = filters;
|
|
87
|
+
}
|
|
90
88
|
const response = await fetch(url, {
|
|
91
89
|
method: "POST",
|
|
92
90
|
headers: {
|
|
@@ -94,7 +92,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
94
92
|
"Authorization": `Bearer ${accessToken.token}`,
|
|
95
93
|
"User-Agent": userAgentProvider(),
|
|
96
94
|
},
|
|
97
|
-
body: JSON.stringify(
|
|
95
|
+
body: JSON.stringify(requestBody),
|
|
98
96
|
});
|
|
99
97
|
if (!response.ok) {
|
|
100
98
|
throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
|
|
@@ -104,32 +102,39 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
104
102
|
content: [{ type: "text", text: result }],
|
|
105
103
|
};
|
|
106
104
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.object({
|
|
119
|
-
"System.TeamProject": z.array(z.string()).optional().describe("Filter by team project"),
|
|
120
|
-
"System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
|
|
121
|
-
"System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
|
|
122
|
-
"System.State": z.array(z.string()).optional().describe("Filter by state"),
|
|
123
|
-
"System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
|
|
124
|
-
})
|
|
125
|
-
.partial()
|
|
126
|
-
.optional(),
|
|
127
|
-
includeFacets: z.boolean().optional(),
|
|
128
|
-
})
|
|
129
|
-
.strict(),
|
|
130
|
-
}, async ({ searchRequest }) => {
|
|
105
|
+
server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
|
|
106
|
+
searchText: z.string().describe("Search text to find in work items"),
|
|
107
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
108
|
+
areaPath: z.array(z.string()).optional().describe("Filter by area paths"),
|
|
109
|
+
workItemType: z.array(z.string()).optional().describe("Filter by work item types"),
|
|
110
|
+
state: z.array(z.string()).optional().describe("Filter by work item states"),
|
|
111
|
+
assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
|
|
112
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
113
|
+
$skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
114
|
+
$top: z.number().default(10).describe("Number of results to return"),
|
|
115
|
+
}, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, $skip, $top }) => {
|
|
131
116
|
const accessToken = await tokenProvider();
|
|
132
117
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
|
118
|
+
const requestBody = {
|
|
119
|
+
searchText,
|
|
120
|
+
includeFacets,
|
|
121
|
+
$skip,
|
|
122
|
+
$top,
|
|
123
|
+
};
|
|
124
|
+
const filters = {};
|
|
125
|
+
if (project && project.length > 0)
|
|
126
|
+
filters["System.TeamProject"] = project;
|
|
127
|
+
if (areaPath && areaPath.length > 0)
|
|
128
|
+
filters["System.AreaPath"] = areaPath;
|
|
129
|
+
if (workItemType && workItemType.length > 0)
|
|
130
|
+
filters["System.WorkItemType"] = workItemType;
|
|
131
|
+
if (state && state.length > 0)
|
|
132
|
+
filters["System.State"] = state;
|
|
133
|
+
if (assignedTo && assignedTo.length > 0)
|
|
134
|
+
filters["System.AssignedTo"] = assignedTo;
|
|
135
|
+
if (Object.keys(filters).length > 0) {
|
|
136
|
+
requestBody.filters = filters;
|
|
137
|
+
}
|
|
133
138
|
const response = await fetch(url, {
|
|
134
139
|
method: "POST",
|
|
135
140
|
headers: {
|
|
@@ -137,7 +142,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
137
142
|
"Authorization": `Bearer ${accessToken.token}`,
|
|
138
143
|
"User-Agent": userAgentProvider(),
|
|
139
144
|
},
|
|
140
|
-
body: JSON.stringify(
|
|
145
|
+
body: JSON.stringify(requestBody),
|
|
141
146
|
});
|
|
142
147
|
if (!response.ok) {
|
|
143
148
|
throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
|
package/dist/tools/workitems.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { batchApiVersion } from "../utils.js";
|
|
5
|
+
import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* Converts Operation enum key to lowercase string for API usage
|
|
8
|
+
* @param operation The Operation enum key (e.g., "Add", "Replace", "Remove")
|
|
9
|
+
* @returns Lowercase string for API usage (e.g., "add", "replace", "remove")
|
|
10
|
+
*/
|
|
11
|
+
function operationToApiString(operation) {
|
|
12
|
+
return operation.toLowerCase();
|
|
13
|
+
}
|
|
5
14
|
const WORKITEM_TOOLS = {
|
|
6
15
|
my_work_items: "wit_my_work_items",
|
|
7
16
|
list_backlogs: "wit_list_backlogs",
|
|
@@ -42,6 +51,10 @@ function getLinkTypeFromName(name) {
|
|
|
42
51
|
return "Microsoft.VSTS.Common.TestedBy-Forward";
|
|
43
52
|
case "tests":
|
|
44
53
|
return "Microsoft.VSTS.Common.TestedBy-Reverse";
|
|
54
|
+
case "affects":
|
|
55
|
+
return "Microsoft.VSTS.Common.Affects-Forward";
|
|
56
|
+
case "affected by":
|
|
57
|
+
return "Microsoft.VSTS.Common.Affects-Reverse";
|
|
45
58
|
default:
|
|
46
59
|
throw new Error(`Unknown link type: ${name}`);
|
|
47
60
|
}
|
|
@@ -131,13 +144,30 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
131
144
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
132
145
|
workItemId: z.number().describe("The ID of the work item to add a comment to."),
|
|
133
146
|
comment: z.string().describe("The text of the comment to add to the work item."),
|
|
134
|
-
|
|
147
|
+
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
148
|
+
}, async ({ project, workItemId, comment, format }) => {
|
|
135
149
|
const connection = await connectionProvider();
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
150
|
+
const orgUrl = connection.serverUrl;
|
|
151
|
+
const accessToken = await tokenProvider();
|
|
152
|
+
const body = {
|
|
153
|
+
text: comment,
|
|
154
|
+
};
|
|
155
|
+
const formatParameter = format === "markdown" ? 0 : 1;
|
|
156
|
+
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"User-Agent": userAgentProvider(),
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(body),
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`Failed to add a work item comment: ${response.statusText}}`);
|
|
167
|
+
}
|
|
168
|
+
const comments = await response.text();
|
|
139
169
|
return {
|
|
140
|
-
content: [{ type: "text", text:
|
|
170
|
+
content: [{ type: "text", text: comments }],
|
|
141
171
|
};
|
|
142
172
|
});
|
|
143
173
|
server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id.", {
|
|
@@ -200,6 +230,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
200
230
|
value: item.areaPath,
|
|
201
231
|
});
|
|
202
232
|
}
|
|
233
|
+
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
234
|
+
ops.push({
|
|
235
|
+
op: "add",
|
|
236
|
+
path: "/fields/System.IterationPath",
|
|
237
|
+
value: item.iterationPath,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
203
240
|
if (item.format && item.format === "Markdown") {
|
|
204
241
|
ops.push({
|
|
205
242
|
op: "add",
|
|
@@ -212,13 +249,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
212
249
|
value: item.format,
|
|
213
250
|
});
|
|
214
251
|
}
|
|
215
|
-
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
216
|
-
ops.push({
|
|
217
|
-
op: "add",
|
|
218
|
-
path: "/fields/System.IterationPath",
|
|
219
|
-
value: item.iterationPath,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
252
|
return {
|
|
223
253
|
method: "PATCH",
|
|
224
254
|
uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
|
|
@@ -254,17 +284,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
254
284
|
}
|
|
255
285
|
});
|
|
256
286
|
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
257
|
-
|
|
287
|
+
projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
|
|
258
288
|
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
|
|
259
289
|
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
260
290
|
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
261
|
-
}, async ({
|
|
291
|
+
}, async ({ projectId, repositoryId, pullRequestId, workItemId }) => {
|
|
262
292
|
try {
|
|
263
293
|
const connection = await connectionProvider();
|
|
264
294
|
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
265
295
|
// Create artifact link relation using vstfs format
|
|
266
296
|
// Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
|
|
267
|
-
const artifactPathValue = `${
|
|
297
|
+
const artifactPathValue = `${projectId}/${repositoryId}/${pullRequestId}`;
|
|
268
298
|
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
|
|
269
299
|
// Use the PATCH document format for adding a relation
|
|
270
300
|
const patchDocument = [
|
|
@@ -281,7 +311,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
281
311
|
},
|
|
282
312
|
];
|
|
283
313
|
// Use the WorkItem API to update the work item with the new relation
|
|
284
|
-
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId,
|
|
314
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, projectId);
|
|
285
315
|
if (!workItem) {
|
|
286
316
|
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
287
317
|
}
|
|
@@ -323,15 +353,20 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
323
353
|
id: z.number().describe("The ID of the work item to update."),
|
|
324
354
|
updates: z
|
|
325
355
|
.array(z.object({
|
|
326
|
-
op: z.enum(["
|
|
356
|
+
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
327
357
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
328
|
-
value: z.string().describe("The new value for the field. This is required for '
|
|
358
|
+
value: z.string().describe("The new value for the field. This is required for 'Add' and 'Replace' operations, and should be omitted for 'Remove' operations."),
|
|
329
359
|
}))
|
|
330
360
|
.describe("An array of field updates to apply to the work item."),
|
|
331
361
|
}, async ({ id, updates }) => {
|
|
332
362
|
const connection = await connectionProvider();
|
|
333
363
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
334
|
-
|
|
364
|
+
// Convert operation names to lowercase for API
|
|
365
|
+
const apiUpdates = updates.map((update) => ({
|
|
366
|
+
...update,
|
|
367
|
+
op: operationToApiString(update.op),
|
|
368
|
+
}));
|
|
369
|
+
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
335
370
|
return {
|
|
336
371
|
content: [{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) }],
|
|
337
372
|
};
|
|
@@ -351,17 +386,33 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
351
386
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
352
387
|
workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
|
|
353
388
|
fields: z
|
|
354
|
-
.
|
|
355
|
-
.describe("
|
|
389
|
+
.array(z.object({
|
|
390
|
+
name: z.string().describe("The name of the field, e.g., 'System.Title'."),
|
|
391
|
+
value: z.string().describe("The value of the field."),
|
|
392
|
+
format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
393
|
+
}))
|
|
394
|
+
.describe("A record of field names and values to set on the new work item. Each fild is the field name and each value is the corresponding value to set for that field."),
|
|
356
395
|
}, async ({ project, workItemType, fields }) => {
|
|
357
396
|
try {
|
|
358
397
|
const connection = await connectionProvider();
|
|
359
398
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
360
|
-
const document =
|
|
399
|
+
const document = fields.map(({ name, value }) => ({
|
|
361
400
|
op: "add",
|
|
362
|
-
path: `/fields/${
|
|
363
|
-
value,
|
|
401
|
+
path: `/fields/${name}`,
|
|
402
|
+
value: value,
|
|
364
403
|
}));
|
|
404
|
+
// Check if any field has format === "Markdown" and add the multilineFieldsFormat operation
|
|
405
|
+
// this should only happen for large text fields, but since we dont't know by field name, lets assume if the users
|
|
406
|
+
// passes a value longer than 50 characters, then we can set the format to Markdown
|
|
407
|
+
fields.forEach(({ name, value, format }) => {
|
|
408
|
+
if (value.length > 50 && format === "Markdown") {
|
|
409
|
+
document.push({
|
|
410
|
+
op: "add",
|
|
411
|
+
path: `/multilineFieldsFormat/${name}`,
|
|
412
|
+
value: "Markdown",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
365
416
|
const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
|
|
366
417
|
if (!newWorkItem) {
|
|
367
418
|
return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
|
|
@@ -381,14 +432,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
381
432
|
server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
|
|
382
433
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
383
434
|
query: z.string().describe("The ID or path of the query to retrieve."),
|
|
384
|
-
expand: z
|
|
435
|
+
expand: z
|
|
436
|
+
.enum(getEnumKeys(QueryExpand))
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
|
|
385
439
|
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
386
440
|
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
387
441
|
useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
|
|
388
442
|
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
|
|
389
443
|
const connection = await connectionProvider();
|
|
390
444
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
391
|
-
const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
|
|
445
|
+
const queryDetails = await workItemApi.getQuery(project, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
|
|
392
446
|
return {
|
|
393
447
|
content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
|
|
394
448
|
};
|
|
@@ -411,10 +465,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
411
465
|
server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
|
|
412
466
|
updates: z
|
|
413
467
|
.array(z.object({
|
|
414
|
-
op: z.enum(["
|
|
468
|
+
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
415
469
|
id: z.number().describe("The ID of the work item to update."),
|
|
416
470
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
417
471
|
value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
|
|
472
|
+
format: z.enum(["Html", "Markdown"]).optional().describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
418
473
|
}))
|
|
419
474
|
.describe("An array of updates to apply to work items. Each update should include the operation (op), work item ID (id), field path (path), and new value (value)."),
|
|
420
475
|
}, async ({ updates }) => {
|
|
@@ -423,20 +478,32 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
423
478
|
const accessToken = await tokenProvider();
|
|
424
479
|
// Extract unique IDs from the updates array
|
|
425
480
|
const uniqueIds = Array.from(new Set(updates.map((update) => update.id)));
|
|
426
|
-
const body = uniqueIds.map((id) =>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
headers: {
|
|
430
|
-
"Content-Type": "application/json-patch+json",
|
|
431
|
-
},
|
|
432
|
-
body: updates
|
|
433
|
-
.filter((update) => update.id === id)
|
|
434
|
-
.map(({ op, path, value }) => ({
|
|
481
|
+
const body = uniqueIds.map((id) => {
|
|
482
|
+
const workItemUpdates = updates.filter((update) => update.id === id);
|
|
483
|
+
const operations = workItemUpdates.map(({ op, path, value }) => ({
|
|
435
484
|
op: op,
|
|
436
485
|
path: path,
|
|
437
486
|
value: value,
|
|
438
|
-
}))
|
|
439
|
-
|
|
487
|
+
}));
|
|
488
|
+
// Add format operations for Markdown fields
|
|
489
|
+
workItemUpdates.forEach(({ path, value, format }) => {
|
|
490
|
+
if (format === "Markdown" && value && value.length > 50) {
|
|
491
|
+
operations.push({
|
|
492
|
+
op: "Add",
|
|
493
|
+
path: `/multilineFieldsFormat${path.replace("/fields", "")}`,
|
|
494
|
+
value: "Markdown",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
method: "PATCH",
|
|
500
|
+
uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json-patch+json",
|
|
503
|
+
},
|
|
504
|
+
body: operations,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
440
507
|
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
441
508
|
method: "PATCH",
|
|
442
509
|
headers: {
|
|
@@ -461,9 +528,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
461
528
|
id: z.number().describe("The ID of the work item to update."),
|
|
462
529
|
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
463
530
|
type: z
|
|
464
|
-
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"])
|
|
531
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
465
532
|
.default("related")
|
|
466
|
-
.describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', '
|
|
533
|
+
.describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'affects', and 'affected by'. Defaults to 'related'."),
|
|
467
534
|
comment: z.string().optional().describe("Optional comment to include with the link. This can be used to provide additional context for the link being created."),
|
|
468
535
|
}))
|
|
469
536
|
.describe(""),
|
package/dist/utils.js
CHANGED
|
@@ -2,3 +2,29 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
export const apiVersion = "7.2-preview.1";
|
|
4
4
|
export const batchApiVersion = "5.0";
|
|
5
|
+
export const markdownCommentsApiVersion = "7.2-preview.4";
|
|
6
|
+
/**
|
|
7
|
+
* Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
|
|
8
|
+
* This ensures that enum schemas generate string values rather than numeric values.
|
|
9
|
+
* @param enumObject The TypeScript enum object
|
|
10
|
+
* @returns Array of string keys from the enum
|
|
11
|
+
*/
|
|
12
|
+
export function getEnumKeys(enumObject) {
|
|
13
|
+
return Object.keys(enumObject).filter((key) => isNaN(Number(key)));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Safely converts a string enum key to its corresponding enum value.
|
|
17
|
+
* Validates that the key exists in the enum before conversion.
|
|
18
|
+
* @param enumObject The TypeScript enum object
|
|
19
|
+
* @param key The string key to convert
|
|
20
|
+
* @returns The enum value if key is valid, undefined otherwise
|
|
21
|
+
*/
|
|
22
|
+
export function safeEnumConvert(enumObject, key) {
|
|
23
|
+
if (!key)
|
|
24
|
+
return undefined;
|
|
25
|
+
const validKeys = getEnumKeys(enumObject);
|
|
26
|
+
if (!validKeys.includes(key)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return enumObject[key];
|
|
30
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "1.2.0
|
|
1
|
+
export const packageVersion = "1.2.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server for interacting with Azure DevOps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
27
27
|
"prepare": "npm run build",
|
|
28
28
|
"watch": "tsc --watch",
|
|
29
|
-
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
29
|
+
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
|
|
30
30
|
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
31
31
|
"eslint": "eslint",
|
|
32
32
|
"eslint-fix": "eslint --fix",
|
|
@@ -37,18 +37,19 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@azure/identity": "^4.10.0",
|
|
40
|
-
"@modelcontextprotocol/sdk": "1.
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.16.0",
|
|
41
41
|
"azure-devops-extension-api": "^4.252.0",
|
|
42
42
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
43
43
|
"azure-devops-node-api": "^15.1.0",
|
|
44
|
+
"yargs": "^18.0.0",
|
|
44
45
|
"zod": "^3.25.63",
|
|
45
46
|
"zod-to-json-schema": "^3.24.5"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@modelcontextprotocol/inspector": "^0.
|
|
49
|
+
"@modelcontextprotocol/inspector": "^0.16.1",
|
|
49
50
|
"@types/jest": "^30.0.0",
|
|
50
51
|
"@types/node": "^22",
|
|
51
|
-
"eslint-config-prettier": "10.1.
|
|
52
|
+
"eslint-config-prettier": "10.1.8",
|
|
52
53
|
"eslint-plugin-header": "^3.1.1",
|
|
53
54
|
"jest": "^30.0.2",
|
|
54
55
|
"jest-extended": "^6.0.0",
|