@azure-devops/mcp 2.9.0-nightly.20260823 → 2.9.0-nightly.20260901
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/dist/auth.js +66 -6
- package/dist/index.js +4 -15
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/tools/repositories.js +25 -8
- package/dist/tools/search.js +6 -11
- package/dist/tools/work-items.js +17 -9
- package/dist/tools.js +0 -0
- package/dist/useragent.js +0 -0
- package/dist/utils.js +13 -0
- package/dist/version.js +1 -1
- package/package.json +10 -3
package/dist/auth.js
CHANGED
|
@@ -2,15 +2,41 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
|
|
4
4
|
import { PublicClientApplication } from "@azure/msal-node";
|
|
5
|
+
import { NativeBrokerPlugin } from "@azure/msal-node-extensions";
|
|
5
6
|
import open from "open";
|
|
6
7
|
import { logger } from "./logger.js";
|
|
7
8
|
const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
|
|
9
|
+
const patAllowedHosts = new Set(["dev.azure.com", "vssps.dev.azure.com", "almsearch.dev.azure.com"]);
|
|
10
|
+
function isPatAllowedHost(hostname) {
|
|
11
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
12
|
+
return patAllowedHosts.has(normalizedHostname) || normalizedHostname.endsWith(".visualstudio.com");
|
|
13
|
+
}
|
|
14
|
+
function installPatFetchInterceptor(basicValue) {
|
|
15
|
+
const originalFetch = globalThis.fetch;
|
|
16
|
+
const patBearerValue = `Bearer ${basicValue}`;
|
|
17
|
+
globalThis.fetch = async (input, init) => {
|
|
18
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
19
|
+
if (headers.get("Authorization") !== patBearerValue) {
|
|
20
|
+
return originalFetch(input, init);
|
|
21
|
+
}
|
|
22
|
+
const requestUrl = new URL(input instanceof Request ? input.url : input.toString());
|
|
23
|
+
if (requestUrl.protocol !== "https:" || !isPatAllowedHost(requestUrl.hostname)) {
|
|
24
|
+
throw new Error(`Refusing to send a Personal Access Token to untrusted destination '${requestUrl.origin}'`);
|
|
25
|
+
}
|
|
26
|
+
headers.set("Authorization", `Basic ${basicValue}`);
|
|
27
|
+
if (input instanceof Request) {
|
|
28
|
+
return originalFetch(new Request(input, { ...init, headers }));
|
|
29
|
+
}
|
|
30
|
+
return originalFetch(input, { ...init, headers });
|
|
31
|
+
};
|
|
32
|
+
}
|
|
8
33
|
class OAuthAuthenticator {
|
|
9
34
|
static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
|
|
10
35
|
static defaultAuthority = "https://login.microsoftonline.com/common";
|
|
11
36
|
static zeroTenantId = "00000000-0000-0000-0000-000000000000";
|
|
12
37
|
accountId;
|
|
13
38
|
publicClientApp;
|
|
39
|
+
publicClientAppFallback;
|
|
14
40
|
constructor(tenantId) {
|
|
15
41
|
this.accountId = null;
|
|
16
42
|
let authority = OAuthAuthenticator.defaultAuthority;
|
|
@@ -26,6 +52,22 @@ class OAuthAuthenticator {
|
|
|
26
52
|
clientId: OAuthAuthenticator.clientId,
|
|
27
53
|
authority,
|
|
28
54
|
},
|
|
55
|
+
broker: {
|
|
56
|
+
nativeBrokerPlugin: new NativeBrokerPlugin(),
|
|
57
|
+
},
|
|
58
|
+
system: {
|
|
59
|
+
loggerOptions: {
|
|
60
|
+
loggerCallback: (level, message) => {
|
|
61
|
+
logger.debug(`MSALClient[${level}]: ${message}`);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
this.publicClientAppFallback = new PublicClientApplication({
|
|
67
|
+
auth: {
|
|
68
|
+
clientId: OAuthAuthenticator.clientId,
|
|
69
|
+
authority,
|
|
70
|
+
},
|
|
29
71
|
});
|
|
30
72
|
logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
|
|
31
73
|
}
|
|
@@ -50,17 +92,35 @@ class OAuthAuthenticator {
|
|
|
50
92
|
}
|
|
51
93
|
if (!authResult) {
|
|
52
94
|
logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
|
|
53
|
-
|
|
95
|
+
try {
|
|
96
|
+
authResult = await this.publicClientApp.acquireTokenInteractive({
|
|
97
|
+
scopes,
|
|
98
|
+
openBrowser: async (url) => {
|
|
99
|
+
logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
|
|
100
|
+
open(url);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.accountId = authResult.account;
|
|
104
|
+
logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const msalErrorMessage = error.platformBrokerError ? JSON.stringify(error.platformBrokerError) : "";
|
|
108
|
+
logger.debug(`OAuthAuthenticator: Interactive token acquisition failed: ${error instanceof Error ? error.message + msalErrorMessage : String(error)}`);
|
|
109
|
+
authResult = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!authResult) {
|
|
113
|
+
logger.debug(`OAuthAuthenticator: Starting interactive token acquisition without broker`);
|
|
114
|
+
authResult = await this.publicClientAppFallback.acquireTokenInteractive({
|
|
54
115
|
scopes,
|
|
55
116
|
openBrowser: async (url) => {
|
|
56
|
-
logger.debug(`OAuthAuthenticator: Opening browser for authentication`);
|
|
117
|
+
logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
|
|
57
118
|
open(url);
|
|
58
119
|
},
|
|
59
120
|
});
|
|
60
|
-
|
|
61
|
-
logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
|
|
121
|
+
logger.debug(`OAuthAuthenticator: Successfully acquired token interactively without broker`);
|
|
62
122
|
}
|
|
63
|
-
if (!authResult
|
|
123
|
+
if (!authResult?.accessToken) {
|
|
64
124
|
logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
|
|
65
125
|
throw new Error("Failed to obtain Azure DevOps OAuth token.");
|
|
66
126
|
}
|
|
@@ -126,4 +186,4 @@ function createAuthenticator(type, tenantId) {
|
|
|
126
186
|
};
|
|
127
187
|
}
|
|
128
188
|
}
|
|
129
|
-
export { createAuthenticator };
|
|
189
|
+
export { createAuthenticator, installPatFetchInterceptor };
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { getBearerHandler, getPersonalAccessTokenHandler, WebApi } from "azure-devops-node-api";
|
|
7
7
|
import yargs from "yargs";
|
|
8
|
-
import {
|
|
9
|
-
import { createAuthenticator } from "./auth.js";
|
|
8
|
+
import { createAuthenticator, installPatFetchInterceptor } from "./auth.js";
|
|
10
9
|
import { logger } from "./logger.js";
|
|
11
10
|
import { getOrgTenant } from "./org-tenants.js";
|
|
12
11
|
//import { configurePrompts } from "./prompts.js";
|
|
13
12
|
import { configureAllTools } from "./tools.js";
|
|
14
13
|
import { UserAgentComposer } from "./useragent.js";
|
|
14
|
+
import { getCliArgs } from "./utils.js";
|
|
15
15
|
import { packageVersion } from "./version.js";
|
|
16
16
|
import { DomainsManager } from "./shared/domains.js";
|
|
17
17
|
function isGitHubCodespaceEnv() {
|
|
@@ -19,7 +19,7 @@ function isGitHubCodespaceEnv() {
|
|
|
19
19
|
}
|
|
20
20
|
const defaultAuthenticationType = isGitHubCodespaceEnv() ? "azcli" : "interactive";
|
|
21
21
|
// Parse command line arguments using yargs
|
|
22
|
-
const argv = yargs(
|
|
22
|
+
const argv = yargs(getCliArgs())
|
|
23
23
|
.scriptName("mcp-server-azuredevops")
|
|
24
24
|
.usage("Usage: $0 <organization> [options]")
|
|
25
25
|
.version(packageVersion)
|
|
@@ -97,18 +97,7 @@ async function main() {
|
|
|
97
97
|
const authenticator = createAuthenticator(argv.authentication, tenantId);
|
|
98
98
|
if (argv.authentication === "pat") {
|
|
99
99
|
const basicValue = await authenticator();
|
|
100
|
-
|
|
101
|
-
const _originalFetch = globalThis.fetch;
|
|
102
|
-
globalThis.fetch = async (input, init) => {
|
|
103
|
-
if (init?.headers) {
|
|
104
|
-
const headers = new Headers(init.headers);
|
|
105
|
-
if (headers.get("Authorization")?.startsWith("Bearer ")) {
|
|
106
|
-
headers.set("Authorization", `Basic ${basicValue}`);
|
|
107
|
-
init = { ...init, headers };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return _originalFetch(input, init);
|
|
111
|
-
};
|
|
100
|
+
installPatFetchInterceptor(basicValue);
|
|
112
101
|
logger.debug("PAT mode: global fetch interceptor installed to rewrite Bearer -> Basic auth headers");
|
|
113
102
|
}
|
|
114
103
|
// removing prompts until further notice
|
package/dist/logger.js
CHANGED
|
File without changes
|
package/dist/org-tenants.js
CHANGED
|
File without changes
|
package/dist/prompts.js
CHANGED
|
File without changes
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
|
|
6
6
|
import { extractAdoStreamError, getEnumKeys, streamToString, apiVersion } from "../utils.js";
|
|
7
7
|
import { orgName } from "../index.js";
|
|
8
|
+
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
8
9
|
const REPO_TOOLS = {
|
|
9
10
|
repo_repository: "repo_repository",
|
|
10
11
|
repo_pull_request: "repo_pull_request",
|
|
@@ -243,7 +244,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
243
244
|
enhancedResponse = { ...enhancedResponse, changedFilesSummary: {} };
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
|
-
return
|
|
247
|
+
return createExternalContentResponse(enhancedResponse, "pull request");
|
|
247
248
|
}
|
|
248
249
|
if (action === "list") {
|
|
249
250
|
if (!repositoryId && !project) {
|
|
@@ -457,7 +458,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
457
458
|
if (streamError) {
|
|
458
459
|
return { content: [{ type: "text", text: `Error getting file content for '${path}': ${streamError}` }], isError: true };
|
|
459
460
|
}
|
|
460
|
-
return
|
|
461
|
+
return createExternalContentResponse(content, "repository file");
|
|
461
462
|
}
|
|
462
463
|
if (action === "list_directory") {
|
|
463
464
|
const versionDescriptor = buildVersionDescriptor(version, versionType === "Commit" ? "Branch" : versionType);
|
|
@@ -750,20 +751,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
750
751
|
// --- repo_pull_request_thread_write ----------------------------------------
|
|
751
752
|
server.tool(REPO_TOOLS.repo_pull_request_thread_write, "Write operations for pull request comment threads. Use the action parameter to specify the operation.", {
|
|
752
753
|
action: z
|
|
753
|
-
.enum(["create", "reply", "update_status"])
|
|
754
|
-
.describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update_status (update the status of a comment thread)."),
|
|
754
|
+
.enum(["create", "reply", "update", "update_status"])
|
|
755
|
+
.describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update (update an existing comment), update_status (update the status of a comment thread)."),
|
|
755
756
|
repositoryId: z.string().describe("The ID or name of the repository. When using a name instead of a GUID, project must also be provided."),
|
|
756
757
|
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
|
|
757
758
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name instead of a GUID."),
|
|
758
|
-
threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply and update_status."),
|
|
759
|
-
|
|
759
|
+
threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply, update, and update_status."),
|
|
760
|
+
commentId: z.coerce.number().min(1).optional().describe("The ID of the comment to update. Required for update."),
|
|
761
|
+
content: z.string().optional().describe("The content of the comment. Required for create, reply, and update."),
|
|
760
762
|
status: z
|
|
761
763
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
762
764
|
.optional()
|
|
763
765
|
.default(CommentThreadStatus[CommentThreadStatus.Active])
|
|
764
766
|
.describe("The thread status. Used for create (defaults to 'Active') and required for update_status."),
|
|
765
767
|
filePath: z.string().optional().describe("The file path for the comment thread. Used for create."),
|
|
766
|
-
fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply."),
|
|
768
|
+
fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply and update."),
|
|
767
769
|
rightFileStartLine: z.coerce.number().min(1).optional().describe("Start line in the right file. Used for create."),
|
|
768
770
|
rightFileStartOffset: z.number().optional().describe("Start character offset in the right file. Used for create."),
|
|
769
771
|
rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create."),
|
|
@@ -771,7 +773,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
771
773
|
changeTrackingId: z.coerce.number().int().min(1).optional().describe("The file change tracking ID from the pull request iteration changes. Used for create."),
|
|
772
774
|
firstComparingIteration: z.coerce.number().int().min(0).optional().describe("The iteration on the left side of the diff. Used for create."),
|
|
773
775
|
secondComparingIteration: z.coerce.number().int().min(1).optional().describe("The iteration on the right side of the diff. Used for create."),
|
|
774
|
-
}, async ({ action, repositoryId, pullRequestId, project, threadId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
|
|
776
|
+
}, async ({ action, repositoryId, pullRequestId, project, threadId, commentId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
|
|
775
777
|
try {
|
|
776
778
|
const connection = await connectionProvider();
|
|
777
779
|
const gitApi = await connection.getGitApi();
|
|
@@ -847,6 +849,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
847
849
|
return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
|
|
848
850
|
return { content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }] };
|
|
849
851
|
}
|
|
852
|
+
if (action === "update") {
|
|
853
|
+
if (!threadId)
|
|
854
|
+
return { content: [{ type: "text", text: "threadId is required for update" }], isError: true };
|
|
855
|
+
if (!commentId)
|
|
856
|
+
return { content: [{ type: "text", text: "commentId is required for update" }], isError: true };
|
|
857
|
+
if (!content)
|
|
858
|
+
return { content: [{ type: "text", text: "content is required for update" }], isError: true };
|
|
859
|
+
const comment = await gitApi.updateComment({ content }, repositoryId, pullRequestId, threadId, commentId, project);
|
|
860
|
+
if (!comment) {
|
|
861
|
+
return { content: [{ type: "text", text: `Error: Failed to update comment ${commentId} in thread ${threadId}. The comment was not updated successfully.` }], isError: true };
|
|
862
|
+
}
|
|
863
|
+
if (fullResponse)
|
|
864
|
+
return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
|
|
865
|
+
return { content: [{ type: "text", text: `Comment ${commentId} successfully updated in thread ${threadId}.` }] };
|
|
866
|
+
}
|
|
850
867
|
if (action === "update_status") {
|
|
851
868
|
if (!threadId)
|
|
852
869
|
return { content: [{ type: "text", text: "threadId is required for update_status" }], isError: true };
|
package/dist/tools/search.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { apiVersion } from "../utils.js";
|
|
5
6
|
import { orgName } from "../index.js";
|
|
6
|
-
import {
|
|
7
|
+
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
7
8
|
const SEARCH_TOOLS = {
|
|
8
9
|
search_code: "search_code",
|
|
9
10
|
search_wiki: "search_wiki",
|
|
@@ -60,9 +61,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
60
61
|
const resultJson = JSON.parse(resultText);
|
|
61
62
|
const gitApi = await connection.getGitApi();
|
|
62
63
|
const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
|
|
63
|
-
return
|
|
64
|
-
content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
|
|
65
|
-
};
|
|
64
|
+
return createExternalContentResponse(resultText + JSON.stringify(combinedResults), "code search results");
|
|
66
65
|
});
|
|
67
66
|
server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
|
|
68
67
|
searchText: z.string().describe("Keywords to search for wiki pages"),
|
|
@@ -101,9 +100,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
101
100
|
throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
|
|
102
101
|
}
|
|
103
102
|
const result = await response.text();
|
|
104
|
-
return
|
|
105
|
-
content: [{ type: "text", text: result }],
|
|
106
|
-
};
|
|
103
|
+
return createExternalContentResponse(result, "wiki search results");
|
|
107
104
|
});
|
|
108
105
|
server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
|
|
109
106
|
searchText: z.string().describe("Search text to find in work items"),
|
|
@@ -151,9 +148,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
151
148
|
throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
|
|
152
149
|
}
|
|
153
150
|
const result = await response.text();
|
|
154
|
-
return
|
|
155
|
-
content: [{ type: "text", text: result }],
|
|
156
|
-
};
|
|
151
|
+
return createExternalContentResponse(result, "work item search results");
|
|
157
152
|
});
|
|
158
153
|
}
|
|
159
154
|
async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
@@ -170,7 +165,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
170
165
|
});
|
|
171
166
|
continue;
|
|
172
167
|
}
|
|
173
|
-
const versionDescriptor =
|
|
168
|
+
const versionDescriptor = { version: changeId, versionType: 2, versionOptions: 0 };
|
|
174
169
|
const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
|
|
175
170
|
false, // latestProcessedChange
|
|
176
171
|
false, // download
|
package/dist/tools/work-items.js
CHANGED
|
@@ -458,9 +458,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
458
458
|
}
|
|
459
459
|
const mimeType = getMimeType(fileName);
|
|
460
460
|
if (mimeType.startsWith("text/")) {
|
|
461
|
-
return
|
|
462
|
-
content: [{ type: "text", text: buffer.toString("utf-8") }],
|
|
463
|
-
};
|
|
461
|
+
return createExternalContentResponse(buffer.toString("utf-8"), "work item attachment");
|
|
464
462
|
}
|
|
465
463
|
const base64Data = buffer.toString("base64");
|
|
466
464
|
return {
|
|
@@ -505,14 +503,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
505
503
|
op: z
|
|
506
504
|
.string()
|
|
507
505
|
.transform((val) => val.toLowerCase())
|
|
508
|
-
.pipe(z.enum(["add", "replace", "remove"]))
|
|
506
|
+
.pipe(z.enum(["add", "replace", "remove", "test"]))
|
|
509
507
|
.default("add")
|
|
510
|
-
.describe("The operation to perform."),
|
|
511
|
-
path: z.string().describe("The
|
|
512
|
-
value: z
|
|
508
|
+
.describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
|
|
509
|
+
path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
|
|
510
|
+
value: z
|
|
511
|
+
.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
512
|
+
.optional()
|
|
513
|
+
.describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
|
|
513
514
|
}))
|
|
514
515
|
.optional()
|
|
515
|
-
.describe(
|
|
516
|
+
.describe('Field updates for a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
|
|
516
517
|
batchUpdates: z
|
|
517
518
|
.array(z.object({
|
|
518
519
|
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
|
|
@@ -575,6 +576,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
575
576
|
return { content: [{ type: "text", text: "id is required for update" }], isError: true };
|
|
576
577
|
if (!updates || updates.length === 0)
|
|
577
578
|
return { content: [{ type: "text", text: "updates is required for update" }], isError: true };
|
|
579
|
+
const updateWithoutValue = updates.find((update) => update.op !== "remove" && update.value === undefined);
|
|
580
|
+
if (updateWithoutValue) {
|
|
581
|
+
return { content: [{ type: "text", text: `value is required for ${updateWithoutValue.op}` }], isError: true };
|
|
582
|
+
}
|
|
578
583
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
579
584
|
const apiUpdates = updates.map((update) => ({ ...update, op: update.op }));
|
|
580
585
|
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
@@ -703,9 +708,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
703
708
|
}
|
|
704
709
|
catch (error) {
|
|
705
710
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
711
|
+
const statusCode = typeof error === "object" && error !== null && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : undefined;
|
|
712
|
+
const statusText = statusCode === 409 ? " Conflict" : statusCode === 412 ? " Precondition Failed" : "";
|
|
713
|
+
const updateStatus = statusCode !== undefined ? ` [HTTP ${statusCode}${statusText}]` : "";
|
|
706
714
|
const msgs = {
|
|
707
715
|
create: `Error creating work item: ${errorMessage}`,
|
|
708
|
-
update: `Error updating work item: ${errorMessage}`,
|
|
716
|
+
update: `Error updating work item${updateStatus}: ${errorMessage}`,
|
|
709
717
|
update_batch: `Error updating work items in batch: ${errorMessage}`,
|
|
710
718
|
add_child: `Error creating child work items: ${errorMessage}`,
|
|
711
719
|
};
|
package/dist/tools.js
CHANGED
|
File without changes
|
package/dist/useragent.js
CHANGED
|
File without changes
|
package/dist/utils.js
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Returns the user-supplied CLI arguments.
|
|
8
|
+
*
|
|
9
|
+
* The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
|
|
10
|
+
* and on Electron hosts alike, so the first two entries are always dropped.
|
|
11
|
+
*
|
|
12
|
+
* Do not replace this with yargs' `hideBin`: it drops a single entry whenever
|
|
13
|
+
* `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
|
|
14
|
+
* host the script path survives and is parsed as the organization name.
|
|
15
|
+
*/
|
|
16
|
+
export function getCliArgs(argv = process.argv) {
|
|
17
|
+
return argv.slice(2);
|
|
18
|
+
}
|
|
6
19
|
export function createEnumMapping(enumObject) {
|
|
7
20
|
const mapping = {};
|
|
8
21
|
for (const [key, value] of Object.entries(enumObject)) {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.9.0-nightly.
|
|
1
|
+
export const packageVersion = "2.9.0-nightly.20260901";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.9.0-nightly.
|
|
3
|
+
"version": "2.9.0-nightly.20260901",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"test": "jest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@azure/identity": "^4.
|
|
41
|
-
"@azure/
|
|
40
|
+
"@azure/identity": "^4.13.0",
|
|
41
|
+
"@azure/logger": "^1.3.0",
|
|
42
|
+
"@azure/msal-node": "^5.5.0",
|
|
43
|
+
"@azure/msal-node-extensions": "^5.3.5",
|
|
42
44
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
43
45
|
"azure-devops-extension-api": "^5.272.3",
|
|
44
46
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
45
47
|
"azure-devops-node-api": "^15.1.2",
|
|
48
|
+
"open": "^10.2.0",
|
|
46
49
|
"winston": "^3.18.3",
|
|
47
50
|
"yargs": "^18.0.0",
|
|
48
51
|
"zod": "^3.25.63",
|
|
@@ -69,5 +72,9 @@
|
|
|
69
72
|
"**/*.(js|ts|jsx|tsx|json|css|md)": [
|
|
70
73
|
"npm run format"
|
|
71
74
|
]
|
|
75
|
+
},
|
|
76
|
+
"allowScripts": {
|
|
77
|
+
"keytar@7.9.0": true,
|
|
78
|
+
"@azure/msal-node-extensions@5.3.5": true
|
|
72
79
|
}
|
|
73
80
|
}
|