@azure-devops/mcp 1.3.1 → 2.0.0-nightly.20250825
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -21
- package/README.md +39 -9
- package/dist/index.js +14 -3
- package/dist/prompts.js +6 -6
- package/dist/shared/domains.js +122 -0
- package/dist/tools/auth.js +44 -2
- package/dist/tools/builds.js +2 -1
- package/dist/tools/core.js +3 -22
- package/dist/tools/{repos.js → repositories.js} +57 -14
- package/dist/tools/{testplans.js → test-plans.js} +4 -1
- package/dist/tools/wiki.js +219 -11
- package/dist/tools/{workitems.js → work-items.js} +160 -10
- package/dist/tools.js +21 -15
- package/dist/useragent.js +0 -0
- package/dist/utils.js +0 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/tools/core.test.js +0 -1
- package/dist/tools/testplan.test.js +0 -125
- package/dist/tools/utils.js +0 -6
- package/dist/tools/wiki.test.js +0 -87
- package/dist/tools/workitem.test.js +0 -101
- package/dist/tools/workitems.test.js +0 -530
- /package/dist/tools/{advsec.js → advanced-security.js} +0 -0
package/dist/tools/wiki.js
CHANGED
|
@@ -6,6 +6,7 @@ const WIKI_TOOLS = {
|
|
|
6
6
|
get_wiki: "wiki_get_wiki",
|
|
7
7
|
list_wiki_pages: "wiki_list_pages",
|
|
8
8
|
get_wiki_page_content: "wiki_get_page_content",
|
|
9
|
+
create_or_update_page: "wiki_create_or_update_page",
|
|
9
10
|
};
|
|
10
11
|
function configureWikiTools(server, tokenProvider, connectionProvider) {
|
|
11
12
|
server.tool(WIKI_TOOLS.get_wiki, "Get the wiki by wikiIdentifier", {
|
|
@@ -84,27 +85,191 @@ function configureWikiTools(server, tokenProvider, connectionProvider) {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
});
|
|
87
|
-
server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content. Provide either a 'url' parameter OR the combination of 'wikiIdentifier' and 'project' parameters.", {
|
|
89
|
+
url: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("The full URL of the wiki page to retrieve content for. If provided, wikiIdentifier, project, and path are ignored. Supported patterns: https://dev.azure.com/{org}/{project}/_wiki/wikis/{wikiIdentifier}?pagePath=%2FMy%20Page and https://dev.azure.com/{org}/{project}/_wiki/wikis/{wikiIdentifier}/{pageId}/Page-Title"),
|
|
93
|
+
wikiIdentifier: z.string().optional().describe("The unique identifier of the wiki. Required if url is not provided."),
|
|
94
|
+
project: z.string().optional().describe("The project name or ID where the wiki is located. Required if url is not provided."),
|
|
95
|
+
path: z.string().optional().describe("The path of the wiki page to retrieve content for. Optional, defaults to root page if not provided."),
|
|
96
|
+
}, async ({ url, wikiIdentifier, project, path }) => {
|
|
92
97
|
try {
|
|
98
|
+
const hasUrl = !!url;
|
|
99
|
+
const hasPair = !!wikiIdentifier && !!project;
|
|
100
|
+
if (hasUrl && hasPair) {
|
|
101
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: Provide either 'url' OR 'wikiIdentifier' with 'project', not both." }], isError: true };
|
|
102
|
+
}
|
|
103
|
+
if (!hasUrl && !hasPair) {
|
|
104
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: You must provide either 'url' OR both 'wikiIdentifier' and 'project'." }], isError: true };
|
|
105
|
+
}
|
|
93
106
|
const connection = await connectionProvider();
|
|
94
107
|
const wikiApi = await connection.getWikiApi();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
let resolvedProject = project;
|
|
109
|
+
let resolvedWiki = wikiIdentifier;
|
|
110
|
+
let resolvedPath = path;
|
|
111
|
+
let pageContent;
|
|
112
|
+
if (url) {
|
|
113
|
+
const parsed = parseWikiUrl(url);
|
|
114
|
+
if ("error" in parsed) {
|
|
115
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: ${parsed.error}` }], isError: true };
|
|
116
|
+
}
|
|
117
|
+
resolvedProject = parsed.project;
|
|
118
|
+
resolvedWiki = parsed.wikiIdentifier;
|
|
119
|
+
if (parsed.pagePath) {
|
|
120
|
+
resolvedPath = parsed.pagePath;
|
|
121
|
+
}
|
|
122
|
+
if (parsed.pageId) {
|
|
123
|
+
try {
|
|
124
|
+
let accessToken;
|
|
125
|
+
try {
|
|
126
|
+
accessToken = await tokenProvider();
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
130
|
+
const restUrl = `${baseUrl}/${resolvedProject}/_apis/wiki/wikis/${resolvedWiki}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
|
|
131
|
+
const resp = await fetch(restUrl, {
|
|
132
|
+
headers: accessToken?.token ? { Authorization: `Bearer ${accessToken.token}` } : {},
|
|
133
|
+
});
|
|
134
|
+
if (resp.ok) {
|
|
135
|
+
const json = await resp.json();
|
|
136
|
+
if (json && typeof json.content === "string") {
|
|
137
|
+
pageContent = json.content;
|
|
138
|
+
}
|
|
139
|
+
else if (json && json.path) {
|
|
140
|
+
resolvedPath = json.path;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (resp.status === 404) {
|
|
144
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: Page with id ${parsed.pageId} not found` }], isError: true };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch { }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!pageContent) {
|
|
151
|
+
if (!resolvedPath) {
|
|
152
|
+
resolvedPath = "/";
|
|
153
|
+
}
|
|
154
|
+
if (!resolvedProject || !resolvedWiki) {
|
|
155
|
+
return { content: [{ type: "text", text: "Project and wikiIdentifier must be defined to fetch wiki page content." }], isError: true };
|
|
156
|
+
}
|
|
157
|
+
const stream = await wikiApi.getPageText(resolvedProject, resolvedWiki, resolvedPath, undefined, undefined, true);
|
|
158
|
+
if (!stream) {
|
|
159
|
+
return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
|
|
160
|
+
}
|
|
161
|
+
pageContent = await streamToString(stream);
|
|
98
162
|
}
|
|
99
|
-
|
|
163
|
+
return { content: [{ type: "text", text: JSON.stringify(pageContent, null, 2) }] };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
100
167
|
return {
|
|
101
|
-
content: [{ type: "text", text:
|
|
168
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${errorMessage}` }],
|
|
169
|
+
isError: true,
|
|
102
170
|
};
|
|
103
171
|
}
|
|
172
|
+
});
|
|
173
|
+
server.tool(WIKI_TOOLS.create_or_update_page, "Create or update a wiki page with content.", {
|
|
174
|
+
wikiIdentifier: z.string().describe("The unique identifier or name of the wiki."),
|
|
175
|
+
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
176
|
+
content: z.string().describe("The content of the wiki page in markdown format."),
|
|
177
|
+
project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."),
|
|
178
|
+
etag: z.string().optional().describe("ETag for editing existing pages (optional, will be fetched if not provided)."),
|
|
179
|
+
}, async ({ wikiIdentifier, path, content, project, etag }) => {
|
|
180
|
+
try {
|
|
181
|
+
const connection = await connectionProvider();
|
|
182
|
+
const accessToken = await tokenProvider();
|
|
183
|
+
// Normalize the path
|
|
184
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
185
|
+
const encodedPath = encodeURIComponent(normalizedPath);
|
|
186
|
+
// Build the URL for the wiki page API
|
|
187
|
+
const baseUrl = connection.serverUrl;
|
|
188
|
+
const projectParam = project || "";
|
|
189
|
+
const url = `${baseUrl}/${projectParam}/_apis/wiki/wikis/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.1`;
|
|
190
|
+
// First, try to create a new page (PUT without ETag)
|
|
191
|
+
try {
|
|
192
|
+
const createResponse = await fetch(url, {
|
|
193
|
+
method: "PUT",
|
|
194
|
+
headers: {
|
|
195
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({ content: content }),
|
|
199
|
+
});
|
|
200
|
+
if (createResponse.ok) {
|
|
201
|
+
const result = await createResponse.json();
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Successfully created wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// If creation failed with 409 (Conflict) or 500 (Page exists), try to update it
|
|
212
|
+
if (createResponse.status === 409 || createResponse.status === 500) {
|
|
213
|
+
// Page exists, we need to get the ETag and update it
|
|
214
|
+
let currentEtag = etag;
|
|
215
|
+
if (!currentEtag) {
|
|
216
|
+
// Fetch current page to get ETag
|
|
217
|
+
const getResponse = await fetch(url, {
|
|
218
|
+
method: "GET",
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${accessToken.token}`,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (getResponse.ok) {
|
|
224
|
+
currentEtag = getResponse.headers.get("etag") || getResponse.headers.get("ETag") || undefined;
|
|
225
|
+
if (!currentEtag) {
|
|
226
|
+
const pageData = await getResponse.json();
|
|
227
|
+
currentEtag = pageData.eTag;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!currentEtag) {
|
|
231
|
+
throw new Error("Could not retrieve ETag for existing page");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Now update the existing page with ETag
|
|
235
|
+
const updateResponse = await fetch(url, {
|
|
236
|
+
method: "PUT",
|
|
237
|
+
headers: {
|
|
238
|
+
"Authorization": `Bearer ${accessToken.token}`,
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
"If-Match": currentEtag,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({ content: content }),
|
|
243
|
+
});
|
|
244
|
+
if (updateResponse.ok) {
|
|
245
|
+
const result = await updateResponse.json();
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: `Successfully updated wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const errorText = await updateResponse.text();
|
|
257
|
+
throw new Error(`Failed to update page (${updateResponse.status}): ${errorText}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const errorText = await createResponse.text();
|
|
262
|
+
throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (fetchError) {
|
|
266
|
+
throw fetchError;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
104
269
|
catch (error) {
|
|
105
270
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
106
271
|
return {
|
|
107
|
-
content: [{ type: "text", text: `Error
|
|
272
|
+
content: [{ type: "text", text: `Error creating/updating wiki page: ${errorMessage}` }],
|
|
108
273
|
isError: true,
|
|
109
274
|
};
|
|
110
275
|
}
|
|
@@ -119,4 +284,47 @@ function streamToString(stream) {
|
|
|
119
284
|
stream.on("error", reject);
|
|
120
285
|
});
|
|
121
286
|
}
|
|
287
|
+
// Helper to parse Azure DevOps wiki page URLs.
|
|
288
|
+
// Supported examples:
|
|
289
|
+
// - https://dev.azure.com/org/project/_wiki/wikis/wikiIdentifier?wikiVersion=GBmain&pagePath=%2FHome
|
|
290
|
+
// - https://dev.azure.com/org/project/_wiki/wikis/wikiIdentifier/123/Title-Of-Page
|
|
291
|
+
// Returns either a structured object OR an error message inside { error }.
|
|
292
|
+
function parseWikiUrl(url) {
|
|
293
|
+
try {
|
|
294
|
+
const u = new URL(url);
|
|
295
|
+
// Path segments after host
|
|
296
|
+
// Expect pattern: /{project}/_wiki/wikis/{wikiIdentifier}[/{pageId}/...]
|
|
297
|
+
const segments = u.pathname.split("/").filter(Boolean); // remove empty
|
|
298
|
+
const idx = segments.findIndex((s) => s === "_wiki");
|
|
299
|
+
if (idx < 1 || segments[idx + 1] !== "wikis") {
|
|
300
|
+
return { error: "URL does not match expected wiki pattern (missing /_wiki/wikis/ segment)." };
|
|
301
|
+
}
|
|
302
|
+
const project = segments[idx - 1];
|
|
303
|
+
const wikiIdentifier = segments[idx + 2];
|
|
304
|
+
if (!project || !wikiIdentifier) {
|
|
305
|
+
return { error: "Could not extract project or wikiIdentifier from URL." };
|
|
306
|
+
}
|
|
307
|
+
// Query form with pagePath
|
|
308
|
+
const pagePathParam = u.searchParams.get("pagePath");
|
|
309
|
+
if (pagePathParam) {
|
|
310
|
+
let decoded = decodeURIComponent(pagePathParam);
|
|
311
|
+
if (!decoded.startsWith("/"))
|
|
312
|
+
decoded = "/" + decoded;
|
|
313
|
+
return { project, wikiIdentifier, pagePath: decoded };
|
|
314
|
+
}
|
|
315
|
+
// Path ID form: .../wikis/{wikiIdentifier}/{pageId}/...
|
|
316
|
+
const afterWiki = segments.slice(idx + 3); // elements after wikiIdentifier
|
|
317
|
+
if (afterWiki.length >= 1) {
|
|
318
|
+
const maybeId = parseInt(afterWiki[0], 10);
|
|
319
|
+
if (!isNaN(maybeId)) {
|
|
320
|
+
return { project, wikiIdentifier, pageId: maybeId };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// If nothing else specified, treat as root page
|
|
324
|
+
return { project, wikiIdentifier, pagePath: "/" };
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return { error: "Invalid URL format." };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
122
330
|
export { WIKI_TOOLS, configureWikiTools };
|
|
@@ -23,6 +23,7 @@ const WORKITEM_TOOLS = {
|
|
|
23
23
|
update_work_items_batch: "wit_update_work_items_batch",
|
|
24
24
|
work_items_link: "wit_work_items_link",
|
|
25
25
|
work_item_unlink: "wit_work_item_unlink",
|
|
26
|
+
add_artifact_link: "wit_add_artifact_link",
|
|
26
27
|
};
|
|
27
28
|
function getLinkTypeFromName(name) {
|
|
28
29
|
switch (name.toLowerCase()) {
|
|
@@ -96,20 +97,37 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
96
97
|
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
|
|
97
98
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
98
99
|
ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
|
|
99
|
-
|
|
100
|
+
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."),
|
|
101
|
+
}, async ({ project, ids, fields }) => {
|
|
100
102
|
const connection = await connectionProvider();
|
|
101
103
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
const defaultFields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
|
|
105
|
+
// If no fields are provided, use the default set of fields
|
|
106
|
+
const fieldsToUse = !fields || fields.length === 0 ? defaultFields : fields;
|
|
107
|
+
const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, project);
|
|
108
|
+
// List of identity fields that need to be transformed from objects to formatted strings
|
|
109
|
+
const identityFields = [
|
|
110
|
+
"System.AssignedTo",
|
|
111
|
+
"System.CreatedBy",
|
|
112
|
+
"System.ChangedBy",
|
|
113
|
+
"System.AuthorizedAs",
|
|
114
|
+
"Microsoft.VSTS.Common.ActivatedBy",
|
|
115
|
+
"Microsoft.VSTS.Common.ResolvedBy",
|
|
116
|
+
"Microsoft.VSTS.Common.ClosedBy",
|
|
117
|
+
];
|
|
118
|
+
// Format identity fields to include displayName and uniqueName
|
|
105
119
|
// Removing the identity object as the response. It's too much and not needed
|
|
106
120
|
if (workitems && Array.isArray(workitems)) {
|
|
107
121
|
workitems.forEach((item) => {
|
|
108
|
-
if (item.fields
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
if (item.fields) {
|
|
123
|
+
identityFields.forEach((fieldName) => {
|
|
124
|
+
if (item.fields && item.fields[fieldName] && typeof item.fields[fieldName] === "object") {
|
|
125
|
+
const identityField = item.fields[fieldName];
|
|
126
|
+
const name = identityField.displayName || "";
|
|
127
|
+
const email = identityField.uniqueName || "";
|
|
128
|
+
item.fields[fieldName] = `${name} <${email}>`.trim();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
113
131
|
}
|
|
114
132
|
});
|
|
115
133
|
}
|
|
@@ -126,7 +144,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
126
144
|
.enum(["all", "fields", "links", "none", "relations"])
|
|
127
145
|
.describe("Optional expand parameter to include additional details in the response.")
|
|
128
146
|
.optional()
|
|
129
|
-
.describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Defaults to 'none'."),
|
|
147
|
+
.describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Relations can be used to get child workitems. Defaults to 'none'."),
|
|
130
148
|
}, async ({ id, project, fields, asOf, expand }) => {
|
|
131
149
|
const connection = await connectionProvider();
|
|
132
150
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
@@ -655,5 +673,137 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
655
673
|
};
|
|
656
674
|
}
|
|
657
675
|
});
|
|
676
|
+
server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically.", {
|
|
677
|
+
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
|
|
678
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
679
|
+
// Option 1: Provide full URI directly
|
|
680
|
+
artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
|
|
681
|
+
// Option 2: Provide individual components to build URI automatically based on linkType
|
|
682
|
+
projectId: z.string().optional().describe("The project ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
683
|
+
repositoryId: z.string().optional().describe("The repository ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
684
|
+
branchName: z.string().optional().describe("The branch name (e.g., 'main'). Required when linkType is 'Branch'."),
|
|
685
|
+
commitId: z.string().optional().describe("The commit SHA hash. Required when linkType is 'Fixed in Commit'."),
|
|
686
|
+
pullRequestId: z.number().optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
687
|
+
buildId: z.number().optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
688
|
+
linkType: z
|
|
689
|
+
.enum([
|
|
690
|
+
"Branch",
|
|
691
|
+
"Build",
|
|
692
|
+
"Fixed in Changeset",
|
|
693
|
+
"Fixed in Commit",
|
|
694
|
+
"Found in build",
|
|
695
|
+
"Integrated in build",
|
|
696
|
+
"Model Link",
|
|
697
|
+
"Pull Request",
|
|
698
|
+
"Related Workitem",
|
|
699
|
+
"Result Attachment",
|
|
700
|
+
"Source Code File",
|
|
701
|
+
"Tag",
|
|
702
|
+
"Test Result",
|
|
703
|
+
"Wiki",
|
|
704
|
+
])
|
|
705
|
+
.default("Branch")
|
|
706
|
+
.describe("Type of artifact link, defaults to 'Branch'. This determines both the link type and how to build the VSTFS URI from individual components."),
|
|
707
|
+
comment: z.string().optional().describe("Comment to include with the artifact link."),
|
|
708
|
+
}, async ({ workItemId, project, artifactUri, projectId, repositoryId, branchName, commitId, pullRequestId, buildId, linkType, comment }) => {
|
|
709
|
+
try {
|
|
710
|
+
const connection = await connectionProvider();
|
|
711
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
712
|
+
let finalArtifactUri;
|
|
713
|
+
if (artifactUri) {
|
|
714
|
+
// Use the provided full URI
|
|
715
|
+
finalArtifactUri = artifactUri;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
// Build the URI from individual components based on linkType
|
|
719
|
+
switch (linkType) {
|
|
720
|
+
case "Branch":
|
|
721
|
+
if (!projectId || !repositoryId || !branchName) {
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: "text", text: "For 'Branch' links, 'projectId', 'repositoryId', and 'branchName' are required." }],
|
|
724
|
+
isError: true,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
finalArtifactUri = `vstfs:///Git/Ref/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2FGB${encodeURIComponent(branchName)}`;
|
|
728
|
+
break;
|
|
729
|
+
case "Fixed in Commit":
|
|
730
|
+
if (!projectId || !repositoryId || !commitId) {
|
|
731
|
+
return {
|
|
732
|
+
content: [{ type: "text", text: "For 'Fixed in Commit' links, 'projectId', 'repositoryId', and 'commitId' are required." }],
|
|
733
|
+
isError: true,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
finalArtifactUri = `vstfs:///Git/Commit/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(commitId)}`;
|
|
737
|
+
break;
|
|
738
|
+
case "Pull Request":
|
|
739
|
+
if (!projectId || !repositoryId || pullRequestId === undefined) {
|
|
740
|
+
return {
|
|
741
|
+
content: [{ type: "text", text: "For 'Pull Request' links, 'projectId', 'repositoryId', and 'pullRequestId' are required." }],
|
|
742
|
+
isError: true,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
finalArtifactUri = `vstfs:///Git/PullRequestId/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(pullRequestId.toString())}`;
|
|
746
|
+
break;
|
|
747
|
+
case "Build":
|
|
748
|
+
case "Found in build":
|
|
749
|
+
case "Integrated in build":
|
|
750
|
+
if (buildId === undefined) {
|
|
751
|
+
return {
|
|
752
|
+
content: [{ type: "text", text: `For '${linkType}' links, 'buildId' is required.` }],
|
|
753
|
+
isError: true,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
finalArtifactUri = `vstfs:///Build/Build/${encodeURIComponent(buildId.toString())}`;
|
|
757
|
+
break;
|
|
758
|
+
default:
|
|
759
|
+
return {
|
|
760
|
+
content: [{ type: "text", text: `URI building from components is not supported for link type '${linkType}'. Please provide the full 'artifactUri' instead.` }],
|
|
761
|
+
isError: true,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Create the patch document for adding an artifact link relation
|
|
766
|
+
const patchDocument = [
|
|
767
|
+
{
|
|
768
|
+
op: "add",
|
|
769
|
+
path: "/relations/-",
|
|
770
|
+
value: {
|
|
771
|
+
rel: "ArtifactLink",
|
|
772
|
+
url: finalArtifactUri,
|
|
773
|
+
attributes: {
|
|
774
|
+
name: linkType,
|
|
775
|
+
...(comment && { comment }),
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
];
|
|
780
|
+
// Use the WorkItem API to update the work item with the new relation
|
|
781
|
+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
|
|
782
|
+
if (!workItem) {
|
|
783
|
+
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: JSON.stringify({
|
|
790
|
+
workItemId,
|
|
791
|
+
artifactUri: finalArtifactUri,
|
|
792
|
+
linkType,
|
|
793
|
+
comment: comment || null,
|
|
794
|
+
success: true,
|
|
795
|
+
}, null, 2),
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
802
|
+
return {
|
|
803
|
+
content: [{ type: "text", text: `Error adding artifact link to work item: ${errorMessage}` }],
|
|
804
|
+
isError: true,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
});
|
|
658
808
|
}
|
|
659
809
|
export { WORKITEM_TOOLS, configureWorkItemTools };
|
package/dist/tools.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import {
|
|
3
|
+
import { Domain } from "./shared/domains.js";
|
|
4
|
+
import { configureAdvSecTools } from "./tools/advanced-security.js";
|
|
4
5
|
import { configureBuildTools } from "./tools/builds.js";
|
|
5
6
|
import { configureCoreTools } from "./tools/core.js";
|
|
6
7
|
import { configureReleaseTools } from "./tools/releases.js";
|
|
7
|
-
import { configureRepoTools } from "./tools/
|
|
8
|
+
import { configureRepoTools } from "./tools/repositories.js";
|
|
8
9
|
import { configureSearchTools } from "./tools/search.js";
|
|
9
|
-
import { configureTestPlanTools } from "./tools/
|
|
10
|
+
import { configureTestPlanTools } from "./tools/test-plans.js";
|
|
10
11
|
import { configureWikiTools } from "./tools/wiki.js";
|
|
11
12
|
import { configureWorkTools } from "./tools/work.js";
|
|
12
|
-
import { configureWorkItemTools } from "./tools/
|
|
13
|
-
function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
import { configureWorkItemTools } from "./tools/work-items.js";
|
|
14
|
+
function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
|
|
15
|
+
const configureIfDomainEnabled = (domain, configureFn) => {
|
|
16
|
+
if (enabledDomains.has(domain)) {
|
|
17
|
+
configureFn();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
21
|
+
configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
|
|
22
|
+
configureIfDomainEnabled(Domain.BUILDS, () => configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
23
|
+
configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
24
|
+
configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
25
|
+
configureIfDomainEnabled(Domain.RELEASES, () => configureReleaseTools(server, tokenProvider, connectionProvider));
|
|
26
|
+
configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider));
|
|
27
|
+
configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider));
|
|
28
|
+
configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
|
|
29
|
+
configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
|
|
24
30
|
}
|
|
25
31
|
export { configureAllTools };
|
package/dist/useragent.js
CHANGED
|
File without changes
|
package/dist/utils.js
CHANGED
|
File without changes
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "
|
|
1
|
+
export const packageVersion = "2.0.0-nightly.20250825";
|
package/package.json
CHANGED
package/dist/tools/core.test.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|