@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 CHANGED
@@ -5,20 +5,20 @@ Easily install the Azure DevOps MCP Server for VS Code or VS Code Insiders:
5
5
  [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-Install_AzureDevops_MCP_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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
  [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_AzureDevops_MCP_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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 defines the **local** MCP server for Azure DevOps, enabling you to perform a wide range of Azure DevOps tasks directly from your code editor.
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. You can expect that the tools will change before general availability.
10
+ > 🚨 **Public Preview:** This project is in public preview. Tools and features may change before general availability.
11
11
 
12
- ## πŸ“„ Table of contents
12
+ ## πŸ“„ Table of Contents
13
13
 
14
14
  1. [πŸ“Ί Overview](#-overview)
15
15
  2. [πŸ† Expectations](#-expectations)
16
- 3. [βš™οΈ Supported tools](#️-supported-tools)
17
- 4. [πŸ”Œ Installation & getting started](#-installation--getting-started)
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 & best practices](#-samples--best-practices)
21
- 8. [πŸ™‹β€β™€οΈ Frequently asked questions](#️-frequently-asked-questions)
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. 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 the complex reasoning.
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 tools
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**: Retrieves a list of work items by IDs in batch.
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**: Retrieves a list of comments for a work item by ID.
68
- - **wit_get_work_items_for_iteration**: Retrieves a list of work items for a specified iteration.
69
- - **wit_add_work_item_comment**: Add comment to a work item by ID.
70
- - **wit_add_child_work_items**: Create one or many child work items of a specific work item type for the given parent Id
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 id.
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 tools
79
+ #### Deprecated Tools
80
80
 
81
- - **wit_add_child_work_item**: Replaced by `wit_add_child_work_items` so that you can create one or many child items per call.
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 Id or Name.
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 my branches for a given repository Id.
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**: Replies to a specific comment on a pull request.
100
- - **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
101
- - **repo_search_commits**: Searches for 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**: Retrieves a list of build definitions for a given project.
106
- - **build_get_definition_revisions**: Retrieves a list of revisions for a specific build definition.
107
- - **build_get_builds**: Retrieves a list of builds for a given project.
108
- - **build_get_log**: Retrieves the logs for a specific build.
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**: Triggers a new build for a specified definition.
112
- - **build_get_status**: Fetches the status of a specific build.
113
- - **build_update_build_stage**: Updates the stage of a specific build.
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**: Retrieves a list of release definitions for a given project.
118
- - **release_get_releases**: Retrieves a list of releases for a given project.
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**: Creates a new test plan in the project.
123
- - **testplan_create_test_case**: Creates a new test case work item.
124
- - **testplan_add_test_cases_to_suite**: Adds existing test cases to a test 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**: Gets a list of test cases in the test plan.
127
- - **testplan_show_test_results_from_build_id**: Gets a list of test results for a given project and 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 the code search results for a given search text.
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 & getting started
135
+ ## πŸ”Œ Installation & Getting Started
136
136
 
137
137
  Clone the repository, install dependencies, and add it to your MCP client configuration.
138
138
 
139
- ### Visual Studio Code & GitHub Copilot
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 install
163
+ #### ✨ One-Click Install
161
164
 
162
165
  [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-Install_AzureDevops_MCP_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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
  [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_AzureDevops_MCP_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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
- #### 🧨 Installing from public feed (recommended)
170
+ #### 🧨 Install from Public Feed (Recommended)
168
171
 
169
- This installation method is the easiest for all users using Visual Studio Code.
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 and add the following:
176
-
177
- ```json
178
- {
179
- "inputs": [
180
- {
181
- "id": "ado_org",
182
- "type": "promptString",
183
- "description": "Azure DevOps organization name (e.g. 'contoso')"
184
- }
185
- ],
186
- "servers": {
187
- "ado": {
188
- "type": "stdio",
189
- "command": "npx",
190
- "args": ["-y", "@azure-devops/mcp", "${input:ado_org}"]
191
- }
192
- }
193
- }
194
- ```
195
-
196
- 2. Save the file, then click 'Start`
197
-
198
- <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
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 that you create a `.github\copilot-instructions.md` in your project and copy and paste 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.
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
- #### πŸ› οΈ Installing from source (dev mode)
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
- ```sh
215
- npm install
216
- ```
217
+
218
+ ```sh
219
+ npm install
220
+ ```
221
+
217
222
  3. Edit or add `.vscode/mcp.json`:
218
223
 
219
- ```json
220
- {
221
- "inputs": [
222
- {
223
- "id": "ado_org",
224
- "type": "promptString",
225
- "description": "Azure DevOps organization's name (e.g. 'contoso')"
226
- }
227
- ],
228
- "servers": {
229
- "ado": {
230
- "type": "stdio",
231
- "command": "mcp-server-azuredevops",
232
- "args": ["${input:ado_org}"]
233
- }
234
- }
235
- }
236
- ```
237
-
238
- 4. Start the Azure DevOps MCP Server
239
-
240
- <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
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 that you create a `.github\copilot-instructions.md` in your project and copy and paste the contents from this [copilot-instructions.md](./.github/copilot-instructions.md) file. This will help your experience when it comes to using the Azure DevOps MCP Server in GitHub Copilot Chat.
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
- See [How To](./docs/HOWTO.md) section for details
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 & best practices
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 asked questions
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 Issues for bugs, enhancements, or documentation improvements.
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 conduct
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
- const args = process.argv.slice(2);
13
- if (args.length === 0) {
14
- console.error("Usage: mcp-server-azuredevops <organization_name>");
15
- process.exit(1);
16
- }
17
- export const orgName = args[0];
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
- const credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
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) {
@@ -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.nativeEnum(DefinitionQueryOrder).optional().describe("Order in which build definitions are returned"),
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.nativeEnum(BuildQueryOrder).default(BuildQueryOrder.QueueTimeDescending).optional().describe("Order in which builds are returned"),
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.nativeEnum(StageUpdateType).describe("New status for the stage"),
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.valueOf(),
187
+ state: safeEnumConvert(StageUpdateType, status),
181
188
  };
182
189
  const response = await fetch(endpoint, {
183
190
  method: "PATCH",
@@ -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.nativeEnum(ReleaseDefinitionExpands).default(ReleaseDefinitionExpands.None).describe("Expand options for release definitions"),
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.nativeEnum(ReleaseDefinitionQueryOrder).default(ReleaseDefinitionQueryOrder.NameAscending).describe("Order of the results"),
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.nativeEnum(ReleaseStatus).optional().default(ReleaseStatus.Active).describe("Status of the releases to filter (default: Active)"),
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.nativeEnum(ReleaseQueryOrder).optional().default(ReleaseQueryOrder.Ascending).describe("Order in which to return releases (default: Ascending)"),
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.nativeEnum(ReleaseExpands).optional().default(ReleaseExpands.None).describe("Expand options for releases"),
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
  };
@@ -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 "abandoned":
36
+ case "Abandoned":
36
37
  return PullRequestStatus.Abandoned.valueOf();
37
- case "active":
38
+ case "Active":
38
39
  return PullRequestStatus.Active.valueOf();
39
- case "all":
40
+ case "All":
40
41
  return PullRequestStatus.All.valueOf();
41
- case "completed":
42
+ case "Completed":
42
43
  return PullRequestStatus.Completed.valueOf();
43
- case "notSet":
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(["active", "abandoned"]).describe("The new status of the pull request. Can be 'active' or 'abandoned'."),
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 === "active" ? 3 : 2;
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.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
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.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
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();
@@ -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
- CODE SEARCH
15
- Get the code search results for a given search text.
16
- */
17
- server.tool(SEARCH_TOOLS.search_code, "Get the code search results for a given search text.", {
18
- searchRequest: z
19
- .object({
20
- searchText: z.string().describe("Search text to find in code"),
21
- $skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
22
- $top: z.number().default(5).describe("Number of results to return (for pagination)"),
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(searchRequest),
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(topResults, gitApi);
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
- WIKI SEARCH
68
- Get wiki search results for a given search text.
69
- */
70
- server.tool(SEARCH_TOOLS.search_wiki, "Get wiki search results for a given search text.", {
71
- searchRequest: z
72
- .object({
73
- searchText: z.string().describe("Search text to find in wikis"),
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(searchRequest),
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
- WORK ITEM SEARCH
109
- Get work item search results for a given search text.
110
- */
111
- server.tool(SEARCH_TOOLS.search_workitem, "Get work item search results for a given search text.", {
112
- searchRequest: z
113
- .object({
114
- searchText: z.string().describe("Search text to find in work items"),
115
- $skip: z.number().default(0).describe("Number of results to skip for pagination"),
116
- $top: z.number().default(10).describe("Number of results to return"),
117
- filters: z
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(searchRequest),
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}`);
@@ -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
- }, async ({ project, workItemId, comment }) => {
147
+ format: z.enum(["markdown", "html"]).optional().default("html"),
148
+ }, async ({ project, workItemId, comment, format }) => {
135
149
  const connection = await connectionProvider();
136
- const workItemApi = await connection.getWorkItemTrackingApi();
137
- const commentCreate = { text: comment };
138
- const commentResponse = await workItemApi.addComment(commentCreate, project, workItemId);
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: JSON.stringify(commentResponse, null, 2) }],
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
- project: z.string().describe("The name or ID of the Azure DevOps project."),
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 ({ project, repositoryId, pullRequestId, workItemId }) => {
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 = `${project}/${repositoryId}/${pullRequestId}`;
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, project);
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(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
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 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
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
- const updatedWorkItem = await workItemApi.updateWorkItem(null, updates, id);
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
- .record(z.string(), z.string())
355
- .describe("A record of field names and values to set on the new work item. Each key is a field name, and each value is the corresponding value to set for that field."),
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 = Object.entries(fields).map(([key, value]) => ({
399
+ const document = fields.map(({ name, value }) => ({
361
400
  op: "add",
362
- path: `/fields/${key}`,
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.enum(["all", "clauses", "minimal", "none", "wiql"]).optional().describe("Optional expand parameter to include additional details in the response. Defaults to 'none'."),
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(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
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
- method: "PATCH",
428
- uri: `/_apis/wit/workitems/${id}?api-version=${batchApiVersion}`,
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', 'referenced by', and 'references'. Defaults to 'related'."),
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-daily.20250715";
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-daily.20250715",
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.13.2",
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.15.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.5",
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",