@azure-devops/mcp 1.2.1 โ†’ 1.3.1-nightly.20250807

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
@@ -15,11 +15,10 @@ This TypeScript project provides a **local** MCP server for Azure DevOps, enabli
15
15
  2. [๐Ÿ† Expectations](#-expectations)
16
16
  3. [โš™๏ธ Supported Tools](#๏ธ-supported-tools)
17
17
  4. [๐Ÿ”Œ Installation & Getting Started](#-installation--getting-started)
18
- 5. [๐Ÿ”ฆ Usage](#-usage)
19
- 6. [๐Ÿ“ Troubleshooting](#-troubleshooting)
20
- 7. [๐ŸŽฉ Samples & Best Practices](#-samples--best-practices)
21
- 8. [๐Ÿ™‹โ€โ™€๏ธ Frequently Asked Questions](#๏ธ-frequently-asked-questions)
22
- 9. [๐Ÿ“Œ Contributing](#-contributing)
18
+ 5. [๐Ÿ“ Troubleshooting](#-troubleshooting)
19
+ 6. [๐ŸŽฉ Examples & Best Practices](#-samples--best-practices)
20
+ 7. [๐Ÿ™‹โ€โ™€๏ธ Frequently Asked Questions](#๏ธ-frequently-asked-questions)
21
+ 8. [๐Ÿ“Œ Contributing](#-contributing)
23
22
 
24
23
  ## ๐Ÿ“บ Overview
25
24
 
@@ -74,6 +73,7 @@ Interact with these Azure DevOps services:
74
73
  - **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
75
74
  - **wit_update_work_items_batch**: Update work items in batch.
76
75
  - **wit_work_items_link**: Link work items together in batch.
76
+ - **wit_work_item_unlink**: Unlink one or many links from a work item.
77
77
 
78
78
  #### Deprecated Tools
79
79
 
@@ -95,6 +95,7 @@ Interact with these Azure DevOps services:
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
97
  - **repo_update_pull_request_status**: Update the status of an existing pull request to active or abandoned.
98
+ - **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch).
98
99
  - **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
99
100
  - **repo_reply_to_comment**: Replies to a specific comment on a pull request.
100
101
  - **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
@@ -118,6 +119,11 @@ Interact with these Azure DevOps services:
118
119
  - **release_get_definitions**: Retrieve a list of release definitions for a given project.
119
120
  - **release_get_releases**: Retrieve a list of releases for a given project.
120
121
 
122
+ ### ๐Ÿ”’ Advanced Security
123
+
124
+ - **advsec_get_alerts**: Retrieve Advanced Security alerts for a repository.
125
+ - **advsec_get_alert_details**: Get detailed information about a specific Advanced Security alert.
126
+
121
127
  ### ๐Ÿงช Test Plans
122
128
 
123
129
  - **testplan_create_test_plan**: Create a new test plan in the project.
@@ -135,14 +141,7 @@ Interact with these Azure DevOps services:
135
141
 
136
142
  ## ๐Ÿ”Œ Installation & Getting Started
137
143
 
138
- Clone the repository, install dependencies, and add it to your MCP client configuration.
139
-
140
- [VS Code and GitHub Copilot](#%EF%B8%8F-visual-studio-code--github-copilot)<br/>
141
- [Visual Studio 2022 and GitHub Copilot](#%EF%B8%8F-visual-studio-2022--github-copilot)
142
-
143
- ### โžก๏ธ Visual Studio Code & GitHub Copilot
144
-
145
- For the best experience, use Visual Studio Code and GitHub Copilot.
144
+ For the best experience, use Visual Studio Code and GitHub Copilot. See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and Cursor.
146
145
 
147
146
  ### Prerequisites
148
147
 
@@ -176,108 +175,7 @@ This installation method is the easiest for all users of Visual Studio Code.
176
175
 
177
176
  ##### Steps
178
177
 
179
- 1. In your project, add a `.vscode\mcp.json` file with the following content:
180
-
181
- ```json
182
- {
183
- "inputs": [
184
- {
185
- "id": "ado_org",
186
- "type": "promptString",
187
- "description": "Azure DevOps organization name (e.g. 'contoso')"
188
- }
189
- ],
190
- "servers": {
191
- "ado": {
192
- "type": "stdio",
193
- "command": "npx",
194
- "args": ["-y", "@azure-devops/mcp", "${input:ado_org}"]
195
- }
196
- }
197
- }
198
- ```
199
-
200
- 2. Save the file, then click 'Start'.
201
-
202
- <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
203
-
204
- 3. In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
205
- 4. Click "Select Tools" and choose the available tools.
206
- 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.
207
-
208
- #### ๐Ÿ› ๏ธ Install from Source (Dev Mode)
209
-
210
- 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.
211
-
212
- > **Note:** For most users, installing from the public feed is simpler and preferred. Use the source installation only if you need the latest changes or are actively contributing to the project.
213
-
214
- ##### Steps
215
-
216
- 1. Clone the repository.
217
- 2. Install dependencies:
218
-
219
- ```sh
220
- npm install
221
- ```
222
-
223
- 3. Edit or add `.vscode/mcp.json`:
224
-
225
- ```json
226
- {
227
- "inputs": [
228
- {
229
- "id": "ado_org",
230
- "type": "promptString",
231
- "description": "Azure DevOps organization's name (e.g. 'contoso')"
232
- }
233
- ],
234
- "servers": {
235
- "ado": {
236
- "type": "stdio",
237
- "command": "mcp-server-azuredevops",
238
- "args": ["${input:ado_org}"]
239
- }
240
- }
241
- }
242
- ```
243
-
244
- 4. Start the Azure DevOps MCP Server.
245
-
246
- <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
247
-
248
- 5. In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
249
- 6. Click "Select Tools" and choose the available tools.
250
- 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.
251
-
252
- See the [How To](./docs/HOWTO.md) section for details.
253
-
254
- ### โžก๏ธ Visual Studio 2022 & GitHub Copilot
255
-
256
- For the best experience, use Visual Studio Code and GitHub Copilot ๐Ÿ‘†.
257
-
258
- ### Prerequisites
259
-
260
- 1. Install [VS Studio 2022 version 17.14](https://learn.microsoft.com/en-us/visualstudio/releases/2022/release-history) or later
261
- 2. Install [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)
262
- 3. Open a project in Visual Studio.
263
-
264
- ### Azure Login
265
-
266
- Ensure you are logged in to Azure DevOps via the Azure CLI:
267
-
268
- ```sh
269
- az login
270
- ```
271
-
272
- #### ๐Ÿงจ Install from Public Feed (Recommended)
273
-
274
- This installation method is the easiest for all users of Visual Studio 2022.
275
-
276
- ๐ŸŽฅ [Watch this quick start video to get up and running in under two minutes!](https://youtu.be/nz_Gn-WL7j0)
277
-
278
- ##### Steps
279
-
280
- 1. Add a `.mcp.json` file to the solution folder with the following content:
178
+ In your project, add a `.vscode\mcp.json` file with the following content:
281
179
 
282
180
  ```json
283
181
  {
@@ -298,45 +196,32 @@ This installation method is the easiest for all users of Visual Studio 2022.
298
196
  }
299
197
  ```
300
198
 
301
- 2. Save the file.
302
- 3. Add your organization name by clicking on the `input` option.
199
+ Save the file, then click 'Start'.
303
200
 
304
- <img src="./docs/media/start-mcp-server-from-vs.png" alt="start mcp server from visual studio 2022" width="250"/>
201
+ <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
305
202
 
306
- 4. Open Copilot chat and switch to [Agent Mode](https://learn.microsoft.com/en-us/visualstudio/ide/copilot-agent-mode?view=vs-2022).
307
- 5. Click the "Tools" icon and choose the available tools.
203
+ In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
308
204
 
309
- <img src="./docs/media/set-tools-from-vs.png" alt="set tools to use in visual studio 2022" width="250"/>
205
+ Click "Select Tools" and choose the available tools.
310
206
 
311
- 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.
207
+ <img src="./docs/media/configure-mcp-server-tools.gif" alt="configure mcp server tools" width="300"/>
312
208
 
313
- ## ๐Ÿ”ฆ Usage
209
+ Open GitHub Copilot Chat and try a prompt like `List ADO projects`.
314
210
 
315
- ### Visual Studio Code + GitHub Copilot
211
+ > ๐Ÿ’ฅ We strongly recommend creating a `.github\copilot-instructions.md` in your project. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
212
+ > To start, just include "`This project uses Azure DevOps. Always check to see if the Azure DevOps MCP server has a tool relevant to the user's request`" in your copilot instructions file.
316
213
 
317
- 1. Open GitHub Copilot in VS Code and switch to Agent mode.
318
- 2. Start the Azure DevOps MCP Server.
319
- 3. The server appears in the tools list.
320
- 4. Try prompts like "List ADO projects".
321
-
322
- ### Visual Studio + GitHub Copilot
323
-
324
- > _Prerequisites:_ Visual Studio 2022 v17.14+, Agent mode enabled in Tools > Options > GitHub > Copilot > Copilot Chat.
325
-
326
- 1. Switch to Agent mode in the Copilot Chat window.
327
- 2. Enter your Azure DevOps organization name.
328
- 3. Select desired `ado` tools.
329
- 4. Try prompts like "List ADO projects".
330
-
331
- 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).
214
+ See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and Cursor.
332
215
 
333
216
  ## ๐Ÿ“ Troubleshooting
334
217
 
335
218
  See the [Troubleshooting guide](./docs/TROUBLESHOOTING.md) for help with common issues and logging.
336
219
 
337
- ## ๐ŸŽฉ Samples & Best Practices
220
+ ## ๐ŸŽฉ Examples & Best Practices
221
+
222
+ Explore example prompts in our [Examples documentation](./docs/EXAMPLES.md).
338
223
 
339
- Find sample prompts and best practices in our [How-to Guide](./docs/HOWTO.md).
224
+ For best practices and tips to enhance your experience with the MCP Server, refer to the [How-To guide](./docs/HOWTO.md).
340
225
 
341
226
  ## ๐Ÿ™‹โ€โ™€๏ธ Frequently Asked Questions
342
227
 
package/dist/index.js CHANGED
File without changes
package/dist/prompts.js CHANGED
File without changes
@@ -0,0 +1,92 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ /**
4
+ * Validates that a name conforms to Claude API requirements.
5
+ * Names must match pattern: ^[a-zA-Z0-9_.-]{1,64}$
6
+ * @param name The name to validate
7
+ * @returns Object with isValid boolean and error/reason message if invalid
8
+ */
9
+ export function validateName(name) {
10
+ // Check length
11
+ if (name.length === 0) {
12
+ return { isValid: false, error: "Name cannot be empty", reason: "name cannot be empty" };
13
+ }
14
+ if (name.length > 64) {
15
+ return {
16
+ isValid: false,
17
+ error: `Name '${name}' is ${name.length} characters long, maximum allowed is 64`,
18
+ reason: `name is ${name.length} characters long, maximum allowed is 64`,
19
+ };
20
+ }
21
+ // Check pattern: only alphanumeric, underscore, dot, and hyphen allowed
22
+ const validPattern = /^[a-zA-Z0-9_.-]+$/;
23
+ if (!validPattern.test(name)) {
24
+ return {
25
+ isValid: false,
26
+ error: `Name '${name}' contains invalid characters. Only alphanumeric characters, underscores, dots, and hyphens are allowed`,
27
+ reason: "name contains invalid characters. Only alphanumeric characters, underscores, dots, and hyphens are allowed",
28
+ };
29
+ }
30
+ return { isValid: true };
31
+ }
32
+ /**
33
+ * Validates that a tool name conforms to Claude API requirements.
34
+ * @param toolName The tool name to validate
35
+ * @returns Object with isValid boolean and error message if invalid
36
+ */
37
+ export function validateToolName(toolName) {
38
+ const result = validateName(toolName);
39
+ if (!result.isValid) {
40
+ return { isValid: false, error: result.error?.replace("Name", "Tool name") };
41
+ }
42
+ return result;
43
+ }
44
+ /**
45
+ * Validates that a parameter name conforms to Claude API requirements.
46
+ * @param paramName The parameter name to validate
47
+ * @returns Object with isValid boolean and error message if invalid
48
+ */
49
+ export function validateParameterName(paramName) {
50
+ const result = validateName(paramName);
51
+ if (!result.isValid) {
52
+ return { isValid: false, error: result.error?.replace("Name", "Parameter name") };
53
+ }
54
+ return result;
55
+ }
56
+ /**
57
+ * Extracts tool names from tool constant definitions
58
+ * @param fileContent - The content of a TypeScript file
59
+ * @returns Array of tool names found
60
+ */
61
+ export function extractToolNames(fileContent) {
62
+ const toolNames = [];
63
+ // Pattern to match tool constant definitions in tool objects
64
+ // This looks for patterns like: const SOMETHING_TOOLS = { ... } or const Test_Plan_Tools = { ... }
65
+ const toolsObjectPattern = /const\s+\w*[Tt][Oo][Oo][Ll][Ss]?\s*=\s*\{([^}]+)\}/g;
66
+ let toolsMatch;
67
+ while ((toolsMatch = toolsObjectPattern.exec(fileContent)) !== null) {
68
+ const objectContent = toolsMatch[1];
69
+ // Now extract individual tool definitions from within the object
70
+ const toolPattern = /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*"([^"]+)"/gm;
71
+ let match;
72
+ while ((match = toolPattern.exec(objectContent)) !== null) {
73
+ toolNames.push(match[1]);
74
+ }
75
+ }
76
+ return toolNames;
77
+ }
78
+ /**
79
+ * Extracts parameter names from Zod schema definitions
80
+ * @param fileContent - The content of a TypeScript file
81
+ * @returns Array of parameter names found
82
+ */
83
+ export function extractParameterNames(fileContent) {
84
+ const paramNames = [];
85
+ // Pattern to match parameter definitions like: paramName: z.string()
86
+ const paramPattern = /^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*z\./gm;
87
+ let match;
88
+ while ((match = paramPattern.exec(fileContent)) !== null) {
89
+ paramNames.push(match[1]);
90
+ }
91
+ return paramNames;
92
+ }
@@ -0,0 +1,108 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { AlertType, AlertValidityStatus, Confidence, Severity, State } from "azure-devops-node-api/interfaces/AlertInterfaces.js";
4
+ import { z } from "zod";
5
+ import { getEnumKeys, mapStringArrayToEnum, mapStringToEnum } from "../utils.js";
6
+ const ADVSEC_TOOLS = {
7
+ get_alerts: "advsec_get_alerts",
8
+ get_alert_details: "advsec_get_alert_details",
9
+ };
10
+ function configureAdvSecTools(server, tokenProvider, connectionProvider) {
11
+ server.tool(ADVSEC_TOOLS.get_alerts, "Retrieve Advanced Security alerts for a repository.", {
12
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
13
+ repository: z.string().describe("The name or ID of the repository to get alerts for."),
14
+ alertType: z
15
+ .enum(getEnumKeys(AlertType))
16
+ .optional()
17
+ .describe("Filter alerts by type. If not specified, returns all alert types."),
18
+ states: z
19
+ .array(z.enum(getEnumKeys(State)))
20
+ .optional()
21
+ .describe("Filter alerts by state. If not specified, returns alerts in any state."),
22
+ severities: z
23
+ .array(z.enum(getEnumKeys(Severity)))
24
+ .optional()
25
+ .describe("Filter alerts by severity level. If not specified, returns alerts at any severity."),
26
+ ruleId: z.string().optional().describe("Filter alerts by rule ID."),
27
+ ruleName: z.string().optional().describe("Filter alerts by rule name."),
28
+ toolName: z.string().optional().describe("Filter alerts by tool name."),
29
+ ref: z.string().optional().describe("Filter alerts by git reference (branch). If not provided and onlyDefaultBranch is true, only includes alerts from default branch."),
30
+ onlyDefaultBranch: z.boolean().optional().default(true).describe("If true, only return alerts found on the default branch. Defaults to true."),
31
+ confidenceLevels: z
32
+ .array(z.enum(getEnumKeys(Confidence)))
33
+ .optional()
34
+ .default(["high", "other"])
35
+ .describe("Filter alerts by confidence levels. Only applicable for secret alerts. Defaults to both 'high' and 'other'."),
36
+ validity: z
37
+ .array(z.enum(getEnumKeys(AlertValidityStatus)))
38
+ .optional()
39
+ .describe("Filter alerts by validity status. Only applicable for secret alerts."),
40
+ top: z.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
41
+ orderBy: z.enum(["id", "firstSeen", "lastSeen", "fixedOn", "severity"]).optional().default("severity").describe("Order results by specified field. Defaults to 'severity'."),
42
+ continuationToken: z.string().optional().describe("Continuation token for pagination."),
43
+ }, async ({ project, repository, alertType, states, severities, ruleId, ruleName, toolName, ref, onlyDefaultBranch, confidenceLevels, validity, top, orderBy, continuationToken }) => {
44
+ try {
45
+ const connection = await connectionProvider();
46
+ const alertApi = await connection.getAlertApi();
47
+ const isSecretAlert = !alertType || alertType.toLowerCase() === "secret";
48
+ const criteria = {
49
+ ...(alertType && { alertType: mapStringToEnum(alertType, AlertType) }),
50
+ ...(states && { states: mapStringArrayToEnum(states, State) }),
51
+ ...(severities && { severities: mapStringArrayToEnum(severities, Severity) }),
52
+ ...(ruleId && { ruleId }),
53
+ ...(ruleName && { ruleName }),
54
+ ...(toolName && { toolName }),
55
+ ...(ref && { ref }),
56
+ ...(onlyDefaultBranch !== undefined && { onlyDefaultBranch }),
57
+ ...(isSecretAlert && confidenceLevels && { confidenceLevels: mapStringArrayToEnum(confidenceLevels, Confidence) }),
58
+ ...(isSecretAlert && validity && { validity: mapStringArrayToEnum(validity, AlertValidityStatus) }),
59
+ };
60
+ const result = await alertApi.getAlerts(project, repository, top, orderBy, criteria, undefined, // expand parameter
61
+ continuationToken);
62
+ return {
63
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
64
+ };
65
+ }
66
+ catch (error) {
67
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Error fetching Advanced Security alerts: ${errorMessage}`,
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ });
79
+ server.tool(ADVSEC_TOOLS.get_alert_details, "Get detailed information about a specific Advanced Security alert.", {
80
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
81
+ repository: z.string().describe("The name or ID of the repository containing the alert."),
82
+ alertId: z.number().describe("The ID of the alert to retrieve details for."),
83
+ ref: z.string().optional().describe("Git reference (branch) to filter the alert."),
84
+ }, async ({ project, repository, alertId, ref }) => {
85
+ try {
86
+ const connection = await connectionProvider();
87
+ const alertApi = await connection.getAlertApi();
88
+ const result = await alertApi.getAlert(project, alertId, repository, ref, undefined // expand parameter
89
+ );
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
92
+ };
93
+ }
94
+ catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: `Error fetching alert details: ${errorMessage}`,
101
+ },
102
+ ],
103
+ isError: true,
104
+ };
105
+ }
106
+ });
107
+ }
108
+ export { ADVSEC_TOOLS, configureAdvSecTools };
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
+ import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
4
  import { z } from "zod";
5
5
  import { getCurrentUserDetails } from "./auth.js";
6
6
  import { getEnumKeys } from "../utils.js";
@@ -16,7 +16,7 @@ const REPO_TOOLS = {
16
16
  get_branch_by_name: "repo_get_branch_by_name",
17
17
  get_pull_request_by_id: "repo_get_pull_request_by_id",
18
18
  create_pull_request: "repo_create_pull_request",
19
- update_pull_request_status: "repo_update_pull_request_status",
19
+ update_pull_request: "repo_update_pull_request",
20
20
  update_pull_request_reviewers: "repo_update_pull_request_reviewers",
21
21
  reply_to_comment: "repo_reply_to_comment",
22
22
  create_pull_request_thread: "repo_create_pull_request_thread",
@@ -82,10 +82,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
82
82
  description: z.string().optional().describe("The description of the pull request. Optional."),
83
83
  isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
84
84
  workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
85
- }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems }) => {
85
+ forkSourceRepositoryId: z.string().optional().describe("The ID of the fork repository that the pull request originates from. Optional, used when creating a pull request from a fork."),
86
+ }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId }) => {
86
87
  const connection = await connectionProvider();
87
88
  const gitApi = await connection.getGitApi();
88
89
  const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
90
+ const forkSource = forkSourceRepositoryId
91
+ ? {
92
+ repository: {
93
+ id: forkSourceRepositoryId,
94
+ },
95
+ }
96
+ : undefined;
89
97
  const pullRequest = await gitApi.createPullRequest({
90
98
  sourceRefName,
91
99
  targetRefName,
@@ -93,20 +101,44 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
93
101
  description,
94
102
  isDraft,
95
103
  workItemRefs: workItemRefs,
104
+ forkSource,
96
105
  }, repositoryId);
97
106
  return {
98
107
  content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
99
108
  };
100
109
  });
101
- server.tool(REPO_TOOLS.update_pull_request_status, "Update status of an existing pull request to active or abandoned.", {
110
+ server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields.", {
102
111
  repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
103
- pullRequestId: z.number().describe("The ID of the pull request to be published."),
104
- status: z.enum(["Active", "Abandoned"]).describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
105
- }, async ({ repositoryId, pullRequestId, status }) => {
112
+ pullRequestId: z.number().describe("The ID of the pull request to update."),
113
+ title: z.string().optional().describe("The new title for the pull request."),
114
+ description: z.string().optional().describe("The new description for the pull request."),
115
+ isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
116
+ targetRefName: z.string().optional().describe("The new target branch name (e.g., 'refs/heads/main')."),
117
+ status: z.enum(["Active", "Abandoned"]).optional().describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
118
+ }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status }) => {
106
119
  const connection = await connectionProvider();
107
120
  const gitApi = await connection.getGitApi();
108
- const statusValue = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
109
- const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
121
+ // Build update object with only provided fields
122
+ const updateRequest = {};
123
+ if (title !== undefined)
124
+ updateRequest.title = title;
125
+ if (description !== undefined)
126
+ updateRequest.description = description;
127
+ if (isDraft !== undefined)
128
+ updateRequest.isDraft = isDraft;
129
+ if (targetRefName !== undefined)
130
+ updateRequest.targetRefName = targetRefName;
131
+ if (status !== undefined) {
132
+ updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
133
+ }
134
+ // Validate that at least one field is provided for update
135
+ if (Object.keys(updateRequest).length === 0) {
136
+ return {
137
+ content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, or status) must be provided for update." }],
138
+ isError: true,
139
+ };
140
+ }
141
+ const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
110
142
  return {
111
143
  content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
112
144
  };
@@ -375,10 +407,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
375
407
  server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
376
408
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
377
409
  pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
378
- }, async ({ repositoryId, pullRequestId }) => {
410
+ includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
411
+ }, async ({ repositoryId, pullRequestId, includeWorkItemRefs }) => {
379
412
  const connection = await connectionProvider();
380
413
  const gitApi = await connection.getGitApi();
381
- const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
414
+ const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
382
415
  return {
383
416
  content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
384
417
  };
@@ -416,6 +449,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
416
449
  content: z.string().describe("The content of the comment to be added."),
417
450
  project: z.string().optional().describe("Project ID or project name (optional)"),
418
451
  filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
452
+ status: z
453
+ .enum(getEnumKeys(CommentThreadStatus))
454
+ .optional()
455
+ .default(CommentThreadStatus[CommentThreadStatus.Active])
456
+ .describe("The status of the comment thread. Defaults to 'Active'."),
419
457
  rightFileStartLine: z.number().optional().describe("Position of first character of the thread's span in right file. The line number of a thread's position. Starts at 1. (optional)"),
420
458
  rightFileStartOffset: z
421
459
  .number()
@@ -429,7 +467,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
429
467
  .number()
430
468
  .optional()
431
469
  .describe("Position of last character of the thread's span in right file. The character offset of a thread's position inside of a line. Must only be set if rightFileEndLine is also specified. (optional)"),
432
- }, async ({ repositoryId, pullRequestId, content, project, filePath, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
470
+ }, async ({ repositoryId, pullRequestId, content, project, filePath, status, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
433
471
  const connection = await connectionProvider();
434
472
  const gitApi = await connection.getGitApi();
435
473
  const threadContext = { filePath: filePath };
@@ -460,7 +498,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
460
498
  threadContext.rightFileEnd.offset = rightFileEndOffset;
461
499
  }
462
500
  }
463
- const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext }, repositoryId, pullRequestId, project);
501
+ const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
464
502
  return {
465
503
  content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
466
504
  };
@@ -79,7 +79,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
79
79
  server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
80
80
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
81
81
  title: z.string().describe("The title of the test case."),
82
- steps: z.string().optional().describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one\\n2. Step two' etc."),
82
+ steps: z.string().optional().describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two"),
83
83
  priority: z.number().optional().describe("The priority of the test case."),
84
84
  areaPath: z.string().optional().describe("The area path for the test case."),
85
85
  iterationPath: z.string().optional().describe("The iteration path for the test case."),
@@ -165,17 +165,21 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
165
165
  * Helper function to convert steps text to XML format required
166
166
  */
167
167
  function convertStepsToXml(steps) {
168
+ // Accepts steps in the format: '1. Step one|Expected result one\n2. Step two|Expected result two'
168
169
  const stepsLines = steps.split("\n").filter((line) => line.trim() !== "");
169
170
  let xmlSteps = `<steps id="0" last="${stepsLines.length}">`;
170
171
  for (let i = 0; i < stepsLines.length; i++) {
171
172
  const stepLine = stepsLines[i].trim();
172
173
  if (stepLine) {
173
- const stepMatch = stepLine.match(/^(\d+)\.\s*(.+)$/);
174
- const stepText = stepMatch ? stepMatch[2] : stepLine;
174
+ // Split step and expected result by '|', fallback to default if not provided
175
+ const [stepPart, expectedPart] = stepLine.split("|").map((s) => s.trim());
176
+ const stepMatch = stepPart.match(/^(\d+)\.\s*(.+)$/);
177
+ const stepText = stepMatch ? stepMatch[2] : stepPart;
178
+ const expectedText = expectedPart || "Verify step completes successfully";
175
179
  xmlSteps += `
176
180
  <step id="${i + 1}" type="ActionStep">
177
181
  <parameterizedString isformatted="true">${escapeXml(stepText)}</parameterizedString>
178
- <parameterizedString isformatted="true">Verify step completes successfully</parameterizedString>
182
+ <parameterizedString isformatted="true">${escapeXml(expectedText)}</parameterizedString>
179
183
  </step>`;
180
184
  }
181
185
  }
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
3
4
  import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
4
5
  import { z } from "zod";
5
6
  import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
@@ -21,6 +22,7 @@ const WORKITEM_TOOLS = {
21
22
  get_query_results_by_id: "wit_get_query_results_by_id",
22
23
  update_work_items_batch: "wit_update_work_items_batch",
23
24
  work_items_link: "wit_work_items_link",
25
+ work_item_unlink: "wit_work_item_unlink",
24
26
  };
25
27
  function getLinkTypeFromName(name) {
26
28
  switch (name.toLowerCase()) {
@@ -46,6 +48,8 @@ function getLinkTypeFromName(name) {
46
48
  return "Microsoft.VSTS.Common.Affects-Forward";
47
49
  case "affected by":
48
50
  return "Microsoft.VSTS.Common.Affects-Reverse";
51
+ case "artifact":
52
+ return "ArtifactLink";
49
53
  default:
50
54
  throw new Error(`Unknown link type: ${name}`);
51
55
  }
@@ -92,11 +96,26 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
92
96
  server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
93
97
  project: z.string().describe("The name or ID of the Azure DevOps project."),
94
98
  ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
95
- }, async ({ project, ids }) => {
99
+ fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, a hardcoded default set of fields will be used."),
100
+ }, async ({ project, ids, fields }) => {
96
101
  const connection = await connectionProvider();
97
102
  const workItemApi = await connection.getWorkItemTrackingApi();
98
- const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags"];
99
- const workitems = await workItemApi.getWorkItemsBatch({ ids, fields }, project);
103
+ const defaultFields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
104
+ // If no fields are provided, use the default set of fields
105
+ const fieldsToUse = !fields || fields.length === 0 ? defaultFields : fields;
106
+ const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, project);
107
+ // Format the assignedTo field to include displayName and uniqueName
108
+ // Removing the identity object as the response. It's too much and not needed
109
+ if (workitems && Array.isArray(workitems)) {
110
+ workitems.forEach((item) => {
111
+ if (item.fields && item.fields["System.AssignedTo"] && typeof item.fields["System.AssignedTo"] === "object") {
112
+ const assignedTo = item.fields["System.AssignedTo"];
113
+ const name = assignedTo.displayName || "";
114
+ const email = assignedTo.uniqueName || "";
115
+ item.fields["System.AssignedTo"] = `${name} <${email}>`.trim();
116
+ }
117
+ });
118
+ }
100
119
  return {
101
120
  content: [{ type: "text", text: JSON.stringify(workitems, null, 2) }],
102
121
  };
@@ -279,13 +298,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
279
298
  repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
280
299
  pullRequestId: z.number().describe("The ID of the pull request to link to."),
281
300
  workItemId: z.number().describe("The ID of the work item to link to the pull request."),
282
- }, async ({ projectId, repositoryId, pullRequestId, workItemId }) => {
301
+ pullRequestProjectId: z.string().optional().describe("The project ID containing the pull request. If not provided, defaults to the work item's project ID (for same-project linking)."),
302
+ }, async ({ projectId, repositoryId, pullRequestId, workItemId, pullRequestProjectId }) => {
283
303
  try {
284
304
  const connection = await connectionProvider();
285
305
  const workItemTrackingApi = await connection.getWorkItemTrackingApi();
286
306
  // Create artifact link relation using vstfs format
287
307
  // Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
288
- const artifactPathValue = `${projectId}/${repositoryId}/${pullRequestId}`;
308
+ const artifactProjectId = pullRequestProjectId && pullRequestProjectId.trim() !== "" ? pullRequestProjectId : projectId;
309
+ const artifactPathValue = `${artifactProjectId}/${repositoryId}/${pullRequestId}`;
289
310
  const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
290
311
  // Use the PATCH document format for adding a relation
291
312
  const patchDocument = [
@@ -573,5 +594,69 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
573
594
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
574
595
  };
575
596
  });
597
+ server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item", {
598
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
599
+ id: z.number().describe("The ID of the work item to remove the links from."),
600
+ type: z
601
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
602
+ .default("related")
603
+ .describe("Type of link to remove. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'affects', 'affected by', and 'artifact'. Defaults to 'related'."),
604
+ url: z.string().optional().describe("Optional URL to match for the link to remove. If not provided, all links of the specified type will be removed."),
605
+ }, async ({ project, id, type, url }) => {
606
+ try {
607
+ const connection = await connectionProvider();
608
+ const workItemApi = await connection.getWorkItemTrackingApi();
609
+ const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations, project);
610
+ const relations = workItem.relations ?? [];
611
+ const linkType = getLinkTypeFromName(type);
612
+ let relationIndexes = [];
613
+ if (url && url.trim().length > 0) {
614
+ // If url is provided, find relations matching both rel type and url
615
+ relationIndexes = relations.map((relation, idx) => (relation.url === url ? idx : -1)).filter((idx) => idx !== -1);
616
+ }
617
+ else {
618
+ // If url is not provided, find all relations matching rel type
619
+ relationIndexes = relations.map((relation, idx) => (relation.rel === linkType ? idx : -1)).filter((idx) => idx !== -1);
620
+ }
621
+ if (relationIndexes.length === 0) {
622
+ return {
623
+ content: [{ type: "text", text: `No matching relations found for link type '${type}'${url ? ` and URL '${url}'` : ""}.\n${JSON.stringify(relations, null, 2)}` }],
624
+ isError: true,
625
+ };
626
+ }
627
+ // Get the relations that will be removed for logging
628
+ const removedRelations = relationIndexes.map((idx) => relations[idx]);
629
+ // Sort indexes in descending order to avoid index shifting when removing
630
+ relationIndexes.sort((a, b) => b - a);
631
+ const apiUpdates = relationIndexes.map((idx) => ({
632
+ op: "remove",
633
+ path: `/relations/${idx}`,
634
+ }));
635
+ const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id, project);
636
+ return {
637
+ content: [
638
+ {
639
+ type: "text",
640
+ text: `Removed ${removedRelations.length} link(s) of type '${type}':\n` +
641
+ JSON.stringify(removedRelations, null, 2) +
642
+ `\n\nUpdated work item result:\n` +
643
+ JSON.stringify(updatedWorkItem, null, 2),
644
+ },
645
+ ],
646
+ isError: false,
647
+ };
648
+ }
649
+ catch (error) {
650
+ return {
651
+ content: [
652
+ {
653
+ type: "text",
654
+ text: `Error unlinking work item: ${error instanceof Error ? error.message : "Unknown error occurred"}`,
655
+ },
656
+ ],
657
+ isError: true,
658
+ };
659
+ }
660
+ });
576
661
  }
577
662
  export { WORKITEM_TOOLS, configureWorkItemTools };
package/dist/tools.js CHANGED
@@ -1,14 +1,15 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { configureCoreTools } from "./tools/core.js";
4
- import { configureWorkTools } from "./tools/work.js";
3
+ import { configureAdvSecTools } from "./tools/advsec.js";
5
4
  import { configureBuildTools } from "./tools/builds.js";
6
- import { configureRepoTools } from "./tools/repos.js";
7
- import { configureWorkItemTools } from "./tools/workitems.js";
5
+ import { configureCoreTools } from "./tools/core.js";
8
6
  import { configureReleaseTools } from "./tools/releases.js";
9
- import { configureWikiTools } from "./tools/wiki.js";
10
- import { configureTestPlanTools } from "./tools/testplans.js";
7
+ import { configureRepoTools } from "./tools/repos.js";
11
8
  import { configureSearchTools } from "./tools/search.js";
9
+ import { configureTestPlanTools } from "./tools/testplans.js";
10
+ import { configureWikiTools } from "./tools/wiki.js";
11
+ import { configureWorkTools } from "./tools/work.js";
12
+ import { configureWorkItemTools } from "./tools/workitems.js";
12
13
  function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider) {
13
14
  configureCoreTools(server, tokenProvider, connectionProvider);
14
15
  configureWorkTools(server, tokenProvider, connectionProvider);
@@ -19,5 +20,6 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
19
20
  configureWikiTools(server, tokenProvider, connectionProvider);
20
21
  configureTestPlanTools(server, tokenProvider, connectionProvider);
21
22
  configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider);
23
+ configureAdvSecTools(server, tokenProvider, connectionProvider);
22
24
  }
23
25
  export { configureAllTools };
package/dist/useragent.js CHANGED
File without changes
package/dist/utils.js CHANGED
@@ -3,6 +3,32 @@
3
3
  export const apiVersion = "7.2-preview.1";
4
4
  export const batchApiVersion = "5.0";
5
5
  export const markdownCommentsApiVersion = "7.2-preview.4";
6
+ export function createEnumMapping(enumObject) {
7
+ const mapping = {};
8
+ for (const [key, value] of Object.entries(enumObject)) {
9
+ if (typeof key === "string" && typeof value === "number") {
10
+ mapping[key.toLowerCase()] = value;
11
+ }
12
+ }
13
+ return mapping;
14
+ }
15
+ export function mapStringToEnum(value, enumObject, defaultValue) {
16
+ if (!value)
17
+ return defaultValue;
18
+ const enumMapping = createEnumMapping(enumObject);
19
+ return enumMapping[value.toLowerCase()] ?? defaultValue;
20
+ }
21
+ /**
22
+ * Maps an array of strings to an array of enum values, filtering out invalid values.
23
+ * @param values Array of string values to map
24
+ * @param enumObject The enum object to map to
25
+ * @returns Array of valid enum values
26
+ */
27
+ export function mapStringArrayToEnum(values, enumObject) {
28
+ if (!values)
29
+ return [];
30
+ return values.map((value) => mapStringToEnum(value, enumObject)).filter((v) => v !== undefined);
31
+ }
6
32
  /**
7
33
  * Converts a TypeScript numeric enum to an array of string keys for use with z.enum().
8
34
  * This ensures that enum schemas generate string values rather than numeric values.
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "1.2.1";
1
+ export const packageVersion = "1.3.1-nightly.20250807";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "1.2.1",
3
+ "version": "1.3.1-nightly.20250807",
4
4
  "description": "MCP server for interacting with Azure DevOps",
5
5
  "license": "MIT",
6
6
  "author": "Microsoft Corporation",
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "preinstall": "npm config set registry https://registry.npmjs.org/",
25
25
  "prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
26
+ "validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
26
27
  "build": "tsc && shx chmod +x dist/*.js",
27
28
  "prepare": "npm run build",
28
29
  "watch": "tsc --watch",
@@ -37,7 +38,7 @@
37
38
  },
38
39
  "dependencies": {
39
40
  "@azure/identity": "^4.10.0",
40
- "@modelcontextprotocol/sdk": "1.16.0",
41
+ "@modelcontextprotocol/sdk": "1.17.0",
41
42
  "azure-devops-extension-api": "^4.252.0",
42
43
  "azure-devops-extension-sdk": "^4.0.2",
43
44
  "azure-devops-node-api": "^15.1.0",
@@ -51,6 +52,7 @@
51
52
  "@types/node": "^22",
52
53
  "eslint-config-prettier": "10.1.8",
53
54
  "eslint-plugin-header": "^3.1.1",
55
+ "glob": "^11.0.3",
54
56
  "jest": "^30.0.2",
55
57
  "jest-extended": "^6.0.0",
56
58
  "prettier": "3.6.2",
package/dist/http.js DELETED
@@ -1,52 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- import express from "express";
4
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
- import { serverBuildAndConnect } from "./server.js";
6
- import { packageVersion } from "./version.js";
7
- const app = express();
8
- app.use(express.json());
9
- app.post('/mcp/:orgName', async (req, res) => {
10
- // In stateless mode, create a new instance of transport and server for each request
11
- // to ensure complete isolation. A single instance would cause request ID collisions
12
- // when multiple clients connect concurrently.
13
- try {
14
- const transport = new StreamableHTTPServerTransport({
15
- sessionIdGenerator: undefined,
16
- });
17
- const server = await serverBuildAndConnect(req.params.orgName, transport);
18
- res.on('close', () => {
19
- transport.close();
20
- server.close();
21
- });
22
- await transport.handleRequest(req, res, req.body);
23
- }
24
- catch (error) {
25
- console.error('Error handling MCP request:', error);
26
- if (!res.headersSent) {
27
- res.status(500).json({
28
- jsonrpc: '2.0',
29
- error: {
30
- code: -32603,
31
- message: 'Internal server error',
32
- },
33
- id: null,
34
- });
35
- }
36
- }
37
- });
38
- app.get('/mcp/:orgName', async (req, res) => {
39
- console.log('Received GET MCP request');
40
- res.writeHead(405).end(JSON.stringify({
41
- jsonrpc: "2.0",
42
- error: {
43
- code: -32000,
44
- message: "Method not allowed."
45
- },
46
- id: null
47
- }));
48
- });
49
- const PORT = 3000;
50
- app.listen(PORT, () => {
51
- console.log(`Azure DevOps MCP Server with http transport listening on port ${PORT}. Version: ${packageVersion}`);
52
- });
package/dist/server.js DELETED
@@ -1,36 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import * as azdev from "azure-devops-node-api";
5
- import { DefaultAzureCredential } from "@azure/identity";
6
- import { configurePrompts } from "./prompts.js";
7
- import { configureAllTools } from "./tools.js";
8
- import { userAgent } from "./utils.js";
9
- import { packageVersion } from "./version.js";
10
- async function getAzureDevOpsToken() {
11
- process.env.AZURE_TOKEN_CREDENTIALS = "dev";
12
- const credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
13
- const token = await credential.getToken("499b84ac-1321-427f-aa17-267ca6975798/.default");
14
- return token;
15
- }
16
- async function getAzureDevOpsClient(orgUrl) {
17
- const token = await getAzureDevOpsToken();
18
- const authHandler = azdev.getBearerHandler(token.token);
19
- const connection = new azdev.WebApi(orgUrl, authHandler, undefined, {
20
- productName: "AzureDevOps.MCP",
21
- productVersion: packageVersion,
22
- userAgent: userAgent
23
- });
24
- return connection;
25
- }
26
- export async function serverBuildAndConnect(orgName, transport) {
27
- const server = new McpServer({
28
- name: "Azure DevOps MCP Server",
29
- version: packageVersion,
30
- });
31
- const orgUrl = "https://dev.azure.com/" + orgName;
32
- configurePrompts(server);
33
- configureAllTools(server, () => orgName, getAzureDevOpsToken, () => getAzureDevOpsClient(orgUrl));
34
- await server.connect(transport);
35
- return server;
36
- }