@azure-devops/mcp 2.8.1-nightly.20260715 → 2.8.1
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/tools/wiki.js +273 -242
- package/dist/tools/work.js +285 -239
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools/wiki.js
CHANGED
|
@@ -4,221 +4,247 @@ import { z } from "zod";
|
|
|
4
4
|
import { apiVersion, extractAdoStreamError, getOrgFromUrl } from "../utils.js";
|
|
5
5
|
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
6
6
|
const WIKI_TOOLS = {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
list_wikis: "wiki_list_wikis",
|
|
8
|
+
get_wiki: "wiki_get_wiki",
|
|
9
|
+
list_wiki_pages: "wiki_list_pages",
|
|
10
|
+
get_wiki_page: "wiki_get_page",
|
|
11
|
+
get_wiki_page_content: "wiki_get_page_content",
|
|
12
|
+
create_or_update_page: "wiki_create_or_update_page",
|
|
9
13
|
};
|
|
10
14
|
function configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
11
|
-
server.tool(WIKI_TOOLS.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
server.tool(WIKI_TOOLS.get_wiki, "Get the wiki by wikiIdentifier", {
|
|
16
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
17
|
+
project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."),
|
|
18
|
+
}, async ({ wikiIdentifier, project }) => {
|
|
19
|
+
try {
|
|
20
|
+
const connection = await connectionProvider();
|
|
21
|
+
const wikiApi = await connection.getWikiApi();
|
|
22
|
+
const wiki = await wikiApi.getWiki(wikiIdentifier, project);
|
|
23
|
+
if (!wiki) {
|
|
24
|
+
return { content: [{ type: "text", text: "No wiki found" }], isError: true };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify(wiki, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: `Error fetching wiki: ${errorMessage}` }],
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
server.tool(WIKI_TOOLS.list_wikis, "Retrieve a list of wikis for an organization or project.", {
|
|
39
|
+
project: z.string().optional().describe("The project name or ID to filter wikis. If not provided, all wikis in the organization will be returned."),
|
|
40
|
+
}, async ({ project }) => {
|
|
41
|
+
try {
|
|
42
|
+
const connection = await connectionProvider();
|
|
43
|
+
const wikiApi = await connection.getWikiApi();
|
|
44
|
+
const wikis = await wikiApi.getAllWikis(project);
|
|
45
|
+
if (!wikis) {
|
|
46
|
+
return { content: [{ type: "text", text: "No wikis found" }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(wikis, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `Error fetching wikis: ${errorMessage}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.tool(WIKI_TOOLS.list_wiki_pages, "Retrieve a list of wiki pages for a specific wiki and project.", {
|
|
61
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
62
|
+
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
63
|
+
top: z.coerce.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
|
|
64
|
+
continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages."),
|
|
65
|
+
pageViewsForDays: z.coerce.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
|
|
66
|
+
}, async ({ wikiIdentifier, project, top = 20, continuationToken, pageViewsForDays }) => {
|
|
67
|
+
try {
|
|
68
|
+
const connection = await connectionProvider();
|
|
69
|
+
const wikiApi = await connection.getWikiApi();
|
|
70
|
+
const pagesBatchRequest = {
|
|
71
|
+
top,
|
|
72
|
+
continuationToken,
|
|
73
|
+
pageViewsForDays,
|
|
74
|
+
};
|
|
75
|
+
const pages = await wikiApi.getPagesBatch(pagesBatchRequest, project, wikiIdentifier);
|
|
76
|
+
if (!pages) {
|
|
77
|
+
return { content: [{ type: "text", text: "No wiki pages found" }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: JSON.stringify(pages, null, 2) }],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: `Error fetching wiki pages: ${errorMessage}` }],
|
|
87
|
+
isError: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.tool(WIKI_TOOLS.get_wiki_page, "Retrieve wiki page metadata by path. This tool does not return page content. Returns isError: true if the page is not found.", {
|
|
92
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
93
|
+
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
94
|
+
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
25
95
|
recursionLevel: z
|
|
26
96
|
.enum(["None", "OneLevel", "OneLevelPlusNestedEmptyFolders", "Full"])
|
|
27
97
|
.optional()
|
|
28
|
-
.describe("Recursion level for subpages.
|
|
29
|
-
}, async ({
|
|
98
|
+
.describe("Recursion level for subpages. 'None' returns only the specified page. 'OneLevel' includes direct children. 'Full' includes all descendants."),
|
|
99
|
+
}, async ({ wikiIdentifier, project, path, recursionLevel }) => {
|
|
30
100
|
try {
|
|
31
101
|
const connection = await connectionProvider();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
102
|
+
const accessToken = await tokenProvider();
|
|
103
|
+
// Normalize the path
|
|
104
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
105
|
+
//const encodedPath = encodeURIComponent(normalizedPath);
|
|
106
|
+
// Build the URL for the wiki page API
|
|
107
|
+
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
108
|
+
const params = new URLSearchParams({
|
|
109
|
+
"path": normalizedPath,
|
|
110
|
+
"api-version": apiVersion,
|
|
111
|
+
});
|
|
112
|
+
if (recursionLevel) {
|
|
113
|
+
params.append("recursionLevel", recursionLevel);
|
|
41
114
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
content: [{ type: "text", text: JSON.stringify(wiki, null, 2) }],
|
|
53
|
-
};
|
|
115
|
+
const url = `${baseUrl}/${encodeURIComponent(project)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?${params.toString()}`;
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
headers: {
|
|
118
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
119
|
+
"User-Agent": userAgentProvider(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const errorText = await response.text();
|
|
124
|
+
throw new Error(`Failed to get wiki page (${response.status}): ${errorText}`);
|
|
54
125
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
126
|
+
const pageData = await response.json();
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: JSON.stringify(pageData, null, 2) }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: `Error fetching wiki page metadata: ${errorMessage}` }],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
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. " + "Returns isError: true if the wiki page is not found.", {
|
|
140
|
+
url: z
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.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"),
|
|
144
|
+
wikiIdentifier: z.string().optional().describe("The unique identifier of the wiki. Required if url is not provided."),
|
|
145
|
+
project: z.string().optional().describe("The project name or ID where the wiki is located. Required if url is not provided."),
|
|
146
|
+
path: z.string().optional().describe("The path of the wiki page to retrieve content for. Optional, defaults to root page if not provided."),
|
|
147
|
+
}, async ({ url, wikiIdentifier, project, path }) => {
|
|
148
|
+
try {
|
|
149
|
+
const hasUrl = !!url;
|
|
150
|
+
const hasPair = !!wikiIdentifier && !!project;
|
|
151
|
+
if (hasUrl && hasPair) {
|
|
152
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: Provide either 'url' OR 'wikiIdentifier' with 'project', not both." }], isError: true };
|
|
75
153
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
return { content: [{ type: "text", text: "wikiIdentifier is required for get_page" }], isError: true };
|
|
79
|
-
}
|
|
80
|
-
if (!project) {
|
|
81
|
-
return { content: [{ type: "text", text: "project is required for get_page" }], isError: true };
|
|
82
|
-
}
|
|
83
|
-
if (!path) {
|
|
84
|
-
return { content: [{ type: "text", text: "path is required for get_page" }], isError: true };
|
|
85
|
-
}
|
|
86
|
-
const accessToken = await tokenProvider();
|
|
87
|
-
// Normalize the path
|
|
88
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
89
|
-
// Build the URL for the wiki page API
|
|
90
|
-
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
91
|
-
const params = new URLSearchParams({
|
|
92
|
-
"path": normalizedPath,
|
|
93
|
-
"api-version": apiVersion,
|
|
94
|
-
});
|
|
95
|
-
if (recursionLevel) {
|
|
96
|
-
params.append("recursionLevel", recursionLevel);
|
|
97
|
-
}
|
|
98
|
-
const fetchUrl = `${baseUrl}/${encodeURIComponent(project)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?${params.toString()}`;
|
|
99
|
-
const response = await fetch(fetchUrl, {
|
|
100
|
-
headers: {
|
|
101
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
102
|
-
"User-Agent": userAgentProvider(),
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
if (!response.ok) {
|
|
106
|
-
const errorText = await response.text();
|
|
107
|
-
throw new Error(`Failed to get wiki page (${response.status}): ${errorText}`);
|
|
108
|
-
}
|
|
109
|
-
const pageData = await response.json();
|
|
110
|
-
return {
|
|
111
|
-
content: [{ type: "text", text: JSON.stringify(pageData, null, 2) }],
|
|
112
|
-
};
|
|
154
|
+
if (!hasUrl && !hasPair) {
|
|
155
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: You must provide either 'url' OR both 'wikiIdentifier' and 'project'." }], isError: true };
|
|
113
156
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const connection = await connectionProvider();
|
|
158
|
+
const wikiApi = await connection.getWikiApi();
|
|
159
|
+
let resolvedProject = project;
|
|
160
|
+
let resolvedWiki = wikiIdentifier;
|
|
161
|
+
let resolvedPath = path;
|
|
162
|
+
let pageContent;
|
|
163
|
+
if (url) {
|
|
164
|
+
const parsed = parseWikiUrl(url);
|
|
165
|
+
if ("error" in parsed) {
|
|
166
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: ${parsed.error}` }], isError: true };
|
|
119
167
|
}
|
|
120
|
-
|
|
121
|
-
|
|
168
|
+
// Guard against cross-organization requests: a user-supplied URL must target the
|
|
169
|
+
// same organization the server is connected to. Otherwise the org segment in the
|
|
170
|
+
// URL would be silently ignored and content fetched from the configured org instead.
|
|
171
|
+
const configuredOrg = getOrgFromUrl(connection.serverUrl);
|
|
172
|
+
const urlOrg = getOrgFromUrl(url);
|
|
173
|
+
if (configuredOrg && urlOrg !== configuredOrg) {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `Error fetching wiki page content: The provided URL targets organization '${urlOrg ?? "unknown"}', which does not match the configured organization '${configuredOrg}'. Cross-organization requests are not allowed.`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
isError: true,
|
|
182
|
+
};
|
|
122
183
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
text: `Error fetching wiki page content: The provided URL targets organization '${urlOrg ?? "unknown"}', which does not match the configured organization '${configuredOrg}'. Cross-organization requests are not allowed.`,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
isError: true,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
resolvedProject = parsed.project;
|
|
150
|
-
resolvedWiki = parsed.wikiIdentifier;
|
|
151
|
-
if (parsed.pagePath) {
|
|
152
|
-
resolvedPath = parsed.pagePath;
|
|
153
|
-
}
|
|
154
|
-
if (parsed.pageId) {
|
|
155
|
-
try {
|
|
156
|
-
const accessToken = await tokenProvider();
|
|
157
|
-
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
158
|
-
const restUrl = `${baseUrl}/${encodeURIComponent(resolvedProject)}/_apis/wiki/wikis/${encodeURIComponent(resolvedWiki)}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
|
|
159
|
-
const resp = await fetch(restUrl, {
|
|
160
|
-
headers: {
|
|
161
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
162
|
-
"User-Agent": userAgentProvider(),
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
if (resp.ok) {
|
|
166
|
-
const json = await resp.json();
|
|
167
|
-
if (json && typeof json.content === "string") {
|
|
168
|
-
pageContent = json.content;
|
|
169
|
-
}
|
|
170
|
-
else if (json && json.path) {
|
|
171
|
-
resolvedPath = json.path;
|
|
172
|
-
}
|
|
184
|
+
resolvedProject = parsed.project;
|
|
185
|
+
resolvedWiki = parsed.wikiIdentifier;
|
|
186
|
+
if (parsed.pagePath) {
|
|
187
|
+
resolvedPath = parsed.pagePath;
|
|
188
|
+
}
|
|
189
|
+
if (parsed.pageId) {
|
|
190
|
+
try {
|
|
191
|
+
const accessToken = await tokenProvider();
|
|
192
|
+
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
193
|
+
const restUrl = `${baseUrl}/${encodeURIComponent(resolvedProject)}/_apis/wiki/wikis/${encodeURIComponent(resolvedWiki)}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
|
|
194
|
+
const resp = await fetch(restUrl, {
|
|
195
|
+
headers: {
|
|
196
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
197
|
+
"User-Agent": userAgentProvider(),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (resp.ok) {
|
|
201
|
+
const json = await resp.json();
|
|
202
|
+
if (json && typeof json.content === "string") {
|
|
203
|
+
pageContent = json.content;
|
|
173
204
|
}
|
|
174
|
-
else if (
|
|
175
|
-
|
|
205
|
+
else if (json && json.path) {
|
|
206
|
+
resolvedPath = json.path;
|
|
176
207
|
}
|
|
177
208
|
}
|
|
178
|
-
|
|
209
|
+
else if (resp.status === 404) {
|
|
210
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: Page with id ${parsed.pageId} not found` }], isError: true };
|
|
211
|
+
}
|
|
179
212
|
}
|
|
213
|
+
catch { }
|
|
180
214
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
215
|
+
}
|
|
216
|
+
if (!pageContent) {
|
|
217
|
+
if (!resolvedPath) {
|
|
218
|
+
resolvedPath = "/";
|
|
219
|
+
}
|
|
220
|
+
// resolvedProject and resolvedWiki are guaranteed to be defined here:
|
|
221
|
+
// - the url branch errors out in parseWikiUrl when project/wikiIdentifier are missing
|
|
222
|
+
// - the pair branch enforces both via the hasPair check above
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
224
|
+
const stream = await wikiApi.getPageText(resolvedProject, resolvedWiki, resolvedPath, undefined, undefined, true);
|
|
225
|
+
if (!stream) {
|
|
226
|
+
return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
|
|
227
|
+
}
|
|
228
|
+
pageContent = await streamToString(stream);
|
|
229
|
+
const streamError = extractAdoStreamError(pageContent);
|
|
230
|
+
if (streamError) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${streamError}` }],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
201
235
|
}
|
|
202
|
-
return createExternalContentResponse(pageContent, "wiki page");
|
|
203
236
|
}
|
|
204
|
-
return
|
|
237
|
+
return createExternalContentResponse(pageContent, "wiki page");
|
|
205
238
|
}
|
|
206
239
|
catch (error) {
|
|
207
240
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
208
|
-
const actionErrorMessages = {
|
|
209
|
-
list_wikis: `Error fetching wikis: ${errorMessage}`,
|
|
210
|
-
get_wiki: `Error fetching wiki: ${errorMessage}`,
|
|
211
|
-
list_pages: `Error fetching wiki pages: ${errorMessage}`,
|
|
212
|
-
get_page: `Error fetching wiki page metadata: ${errorMessage}`,
|
|
213
|
-
get_page_content: `Error fetching wiki page content: ${errorMessage}`,
|
|
214
|
-
};
|
|
215
241
|
return {
|
|
216
|
-
content: [{ type: "text", text:
|
|
242
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${errorMessage}` }],
|
|
217
243
|
isError: true,
|
|
218
244
|
};
|
|
219
245
|
}
|
|
220
246
|
});
|
|
221
|
-
server.tool(WIKI_TOOLS.
|
|
247
|
+
server.tool(WIKI_TOOLS.create_or_update_page, "Create or update a wiki page with content.", {
|
|
222
248
|
wikiIdentifier: z.string().describe("The unique identifier or name of the wiki."),
|
|
223
249
|
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
224
250
|
content: z.string().describe("The content of the wiki page in markdown format."),
|
|
@@ -237,80 +263,85 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
237
263
|
const projectParam = project || "";
|
|
238
264
|
const url = `${baseUrl}/${encodeURIComponent(projectParam)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
|
|
239
265
|
// First, try to create a new page (PUT without ETag)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
headers: {
|
|
243
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
244
|
-
"Content-Type": "application/json",
|
|
245
|
-
"User-Agent": userAgentProvider(),
|
|
246
|
-
},
|
|
247
|
-
body: JSON.stringify({ content: content }),
|
|
248
|
-
});
|
|
249
|
-
if (createResponse.ok) {
|
|
250
|
-
const result = await createResponse.json();
|
|
251
|
-
return {
|
|
252
|
-
content: [
|
|
253
|
-
{
|
|
254
|
-
type: "text",
|
|
255
|
-
text: `Successfully created wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
256
|
-
},
|
|
257
|
-
],
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
// If creation failed with 409 (Conflict) or 500 (Page exists), try to update it
|
|
261
|
-
if (createResponse.status === 409 || createResponse.status === 500) {
|
|
262
|
-
// Page exists, we need to get the ETag and update it
|
|
263
|
-
let currentEtag = etag;
|
|
264
|
-
if (!currentEtag) {
|
|
265
|
-
// Fetch current page to get ETag
|
|
266
|
-
const getResponse = await fetch(url, {
|
|
267
|
-
method: "GET",
|
|
268
|
-
headers: {
|
|
269
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
270
|
-
"User-Agent": userAgentProvider(),
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
if (getResponse.ok) {
|
|
274
|
-
currentEtag = getResponse.headers.get("etag") || getResponse.headers.get("ETag") || undefined;
|
|
275
|
-
if (!currentEtag) {
|
|
276
|
-
const pageData = await getResponse.json();
|
|
277
|
-
currentEtag = pageData.eTag;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (!currentEtag) {
|
|
281
|
-
throw new Error("Could not retrieve ETag for existing page");
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// Now update the existing page with ETag
|
|
285
|
-
const updateResponse = await fetch(url, {
|
|
266
|
+
try {
|
|
267
|
+
const createResponse = await fetch(url, {
|
|
286
268
|
method: "PUT",
|
|
287
269
|
headers: {
|
|
288
270
|
"Authorization": `Bearer ${accessToken}`,
|
|
289
271
|
"Content-Type": "application/json",
|
|
290
272
|
"User-Agent": userAgentProvider(),
|
|
291
|
-
"If-Match": currentEtag,
|
|
292
273
|
},
|
|
293
274
|
body: JSON.stringify({ content: content }),
|
|
294
275
|
});
|
|
295
|
-
if (
|
|
296
|
-
const result = await
|
|
276
|
+
if (createResponse.ok) {
|
|
277
|
+
const result = await createResponse.json();
|
|
297
278
|
return {
|
|
298
279
|
content: [
|
|
299
280
|
{
|
|
300
281
|
type: "text",
|
|
301
|
-
text: `Successfully
|
|
282
|
+
text: `Successfully created wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
302
283
|
},
|
|
303
284
|
],
|
|
304
285
|
};
|
|
305
286
|
}
|
|
287
|
+
// If creation failed with 409 (Conflict) or 500 (Page exists), try to update it
|
|
288
|
+
if (createResponse.status === 409 || createResponse.status === 500) {
|
|
289
|
+
// Page exists, we need to get the ETag and update it
|
|
290
|
+
let currentEtag = etag;
|
|
291
|
+
if (!currentEtag) {
|
|
292
|
+
// Fetch current page to get ETag
|
|
293
|
+
const getResponse = await fetch(url, {
|
|
294
|
+
method: "GET",
|
|
295
|
+
headers: {
|
|
296
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
297
|
+
"User-Agent": userAgentProvider(),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
if (getResponse.ok) {
|
|
301
|
+
currentEtag = getResponse.headers.get("etag") || getResponse.headers.get("ETag") || undefined;
|
|
302
|
+
if (!currentEtag) {
|
|
303
|
+
const pageData = await getResponse.json();
|
|
304
|
+
currentEtag = pageData.eTag;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!currentEtag) {
|
|
308
|
+
throw new Error("Could not retrieve ETag for existing page");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Now update the existing page with ETag
|
|
312
|
+
const updateResponse = await fetch(url, {
|
|
313
|
+
method: "PUT",
|
|
314
|
+
headers: {
|
|
315
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
316
|
+
"Content-Type": "application/json",
|
|
317
|
+
"User-Agent": userAgentProvider(),
|
|
318
|
+
"If-Match": currentEtag,
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify({ content: content }),
|
|
321
|
+
});
|
|
322
|
+
if (updateResponse.ok) {
|
|
323
|
+
const result = await updateResponse.json();
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: `Successfully updated wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const errorText = await updateResponse.text();
|
|
335
|
+
throw new Error(`Failed to update page (${updateResponse.status}): ${errorText}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
306
338
|
else {
|
|
307
|
-
const errorText = await
|
|
308
|
-
throw new Error(`Failed to
|
|
339
|
+
const errorText = await createResponse.text();
|
|
340
|
+
throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`);
|
|
309
341
|
}
|
|
310
342
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`);
|
|
343
|
+
catch (fetchError) {
|
|
344
|
+
throw fetchError;
|
|
314
345
|
}
|
|
315
346
|
}
|
|
316
347
|
catch (error) {
|
package/dist/tools/work.js
CHANGED
|
@@ -4,284 +4,244 @@ import { z } from "zod";
|
|
|
4
4
|
import { TreeStructureGroup, TreeNodeStructureType } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
5
|
import { elicitProject, elicitTeam } from "../shared/elicitations.js";
|
|
6
6
|
const WORK_TOOLS = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
list_team_iterations: "work_list_team_iterations",
|
|
8
|
+
list_iterations: "work_list_iterations",
|
|
9
|
+
create_iterations: "work_create_iterations",
|
|
10
|
+
assign_iterations: "work_assign_iterations",
|
|
11
|
+
get_team_capacity: "work_get_team_capacity",
|
|
12
|
+
update_team_capacity: "work_update_team_capacity",
|
|
13
|
+
get_iteration_capacities: "work_get_iteration_capacities",
|
|
14
|
+
get_team_settings: "work_get_team_settings",
|
|
10
15
|
};
|
|
11
16
|
function configureWorkTools(server, _, connectionProvider) {
|
|
12
|
-
server.tool(WORK_TOOLS.
|
|
13
|
-
action: z
|
|
14
|
-
.enum(["list_iterations", "list_team_iterations", "get_team_settings", "get_team_capacity", "get_iteration_capacities"])
|
|
15
|
-
.describe("The action to perform. Options: list_iterations (list all iterations in a project), list_team_iterations (list iterations assigned to a team), get_team_settings (get team settings including default iteration and area path), get_team_capacity (get team capacity for an iteration), get_iteration_capacities (get capacity for all teams in an iteration)."),
|
|
17
|
+
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project. If a project or team is not specified, you will be prompted to select one.", {
|
|
16
18
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
17
|
-
team: z
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.describe("The name or ID of the Azure DevOps team. Required for list_team_iterations, get_team_settings, and get_team_capacity. Reuse from prior context if already known."),
|
|
21
|
-
iterationId: z.string().optional().describe("The Iteration ID. Required for get_team_capacity and get_iteration_capacities."),
|
|
22
|
-
timeframe: z.enum(["current"]).optional().describe("The timeframe for list_team_iterations. Only 'current' is supported."),
|
|
23
|
-
depth: z.coerce.number().default(2).describe("Depth of children to fetch. Used for list_iterations. Defaults to 2."),
|
|
24
|
-
excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and their children, to exclude from results. Used for list_iterations."),
|
|
25
|
-
}, async ({ action, project, team, iterationId, timeframe, depth, excludedIds }) => {
|
|
19
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
20
|
+
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
|
|
21
|
+
}, async ({ project, team, timeframe }) => {
|
|
26
22
|
try {
|
|
27
23
|
const connection = await connectionProvider();
|
|
28
24
|
let resolvedProject = project;
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
resolvedProject = result.resolved;
|
|
35
|
-
}
|
|
36
|
-
let resolvedTeam = team;
|
|
37
|
-
if (!resolvedTeam) {
|
|
38
|
-
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list iterations for.");
|
|
39
|
-
if ("response" in result)
|
|
40
|
-
return result.response;
|
|
41
|
-
resolvedTeam = result.resolved;
|
|
42
|
-
}
|
|
43
|
-
const workApi = await connection.getWorkApi();
|
|
44
|
-
const iterations = await workApi.getTeamIterations({ project: resolvedProject, team: resolvedTeam }, timeframe);
|
|
45
|
-
if (!iterations) {
|
|
46
|
-
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
content: [
|
|
50
|
-
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
51
|
-
{ type: "text", text: JSON.stringify(iterations, null, 2) },
|
|
52
|
-
],
|
|
53
|
-
};
|
|
25
|
+
if (!resolvedProject) {
|
|
26
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list team iterations for.");
|
|
27
|
+
if ("response" in result)
|
|
28
|
+
return result.response;
|
|
29
|
+
resolvedProject = result.resolved;
|
|
54
30
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
63
|
-
const effectiveDepth = depth ?? 1;
|
|
64
|
-
const results = await workItemTrackingApi.getClassificationNodes(resolvedProject, [], effectiveDepth);
|
|
65
|
-
if (!results) {
|
|
66
|
-
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
67
|
-
}
|
|
68
|
-
let filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
|
|
69
|
-
if (excludedIds && excludedIds.length > 0) {
|
|
70
|
-
const filterOutIds = (nodes) => {
|
|
71
|
-
return nodes
|
|
72
|
-
.filter((node) => !node.id || !excludedIds.includes(node.id))
|
|
73
|
-
.map((node) => {
|
|
74
|
-
if (node.children && node.children.length > 0) {
|
|
75
|
-
return {
|
|
76
|
-
...node,
|
|
77
|
-
children: filterOutIds(node.children),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return node;
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
filteredResults = filterOutIds(filteredResults);
|
|
84
|
-
}
|
|
85
|
-
if (filteredResults.length === 0) {
|
|
86
|
-
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
|
|
90
|
-
};
|
|
31
|
+
let resolvedTeam = team;
|
|
32
|
+
if (!resolvedTeam) {
|
|
33
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list iterations for.");
|
|
34
|
+
if ("response" in result)
|
|
35
|
+
return result.response;
|
|
36
|
+
resolvedTeam = result.resolved;
|
|
91
37
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return result.response;
|
|
97
|
-
resolvedProject = result.resolved;
|
|
98
|
-
}
|
|
99
|
-
let resolvedTeam = team;
|
|
100
|
-
if (!resolvedTeam) {
|
|
101
|
-
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to get settings for.");
|
|
102
|
-
if ("response" in result)
|
|
103
|
-
return result.response;
|
|
104
|
-
resolvedTeam = result.resolved;
|
|
105
|
-
}
|
|
106
|
-
const workApi = await connection.getWorkApi();
|
|
107
|
-
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
108
|
-
const teamSettings = await workApi.getTeamSettings(teamContext);
|
|
109
|
-
if (!teamSettings) {
|
|
110
|
-
return { content: [{ type: "text", text: "No team settings found" }], isError: true };
|
|
111
|
-
}
|
|
112
|
-
const teamFieldValues = await workApi.getTeamFieldValues(teamContext);
|
|
113
|
-
const settingsResult = {
|
|
114
|
-
backlogIteration: teamSettings.backlogIteration,
|
|
115
|
-
defaultIteration: teamSettings.defaultIteration,
|
|
116
|
-
defaultIterationMacro: teamSettings.defaultIterationMacro,
|
|
117
|
-
backlogVisibilities: teamSettings.backlogVisibilities,
|
|
118
|
-
bugsBehavior: teamSettings.bugsBehavior,
|
|
119
|
-
workingDays: teamSettings.workingDays,
|
|
120
|
-
defaultAreaPath: teamFieldValues?.defaultValue,
|
|
121
|
-
areaPathField: teamFieldValues?.field,
|
|
122
|
-
areaPaths: teamFieldValues?.values,
|
|
123
|
-
};
|
|
124
|
-
return {
|
|
125
|
-
content: [
|
|
126
|
-
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
127
|
-
{ type: "text", text: JSON.stringify(settingsResult, null, 2) },
|
|
128
|
-
],
|
|
129
|
-
};
|
|
38
|
+
const workApi = await connection.getWorkApi();
|
|
39
|
+
const iterations = await workApi.getTeamIterations({ project: resolvedProject, team: resolvedTeam }, timeframe);
|
|
40
|
+
if (!iterations) {
|
|
41
|
+
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
|
|
130
42
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
46
|
+
{ type: "text", text: JSON.stringify(iterations, null, 2) },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: `Error fetching team iterations: ${errorMessage}` }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
server.tool(WORK_TOOLS.create_iterations, "Create new iterations in a specified Azure DevOps project.", {
|
|
59
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
60
|
+
iterations: z
|
|
61
|
+
.array(z.object({
|
|
62
|
+
iterationName: z.string().describe("The name of the iteration to create."),
|
|
63
|
+
startDate: z.string().optional().describe("The start date of the iteration in ISO format (e.g., '2023-01-01T00:00:00Z'). Optional."),
|
|
64
|
+
finishDate: z.string().optional().describe("The finish date of the iteration in ISO format (e.g., '2023-01-31T23:59:59Z'). Optional."),
|
|
65
|
+
}))
|
|
66
|
+
.describe("An array of iterations to create. Each iteration must have a name and can optionally have start and finish dates in ISO format."),
|
|
67
|
+
}, async ({ project, iterations }) => {
|
|
68
|
+
try {
|
|
69
|
+
const connection = await connectionProvider();
|
|
70
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const { iterationName, startDate, finishDate } of iterations) {
|
|
73
|
+
// Step 1: Create the iteration
|
|
74
|
+
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode({
|
|
75
|
+
name: iterationName,
|
|
76
|
+
attributes: {
|
|
77
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
78
|
+
finishDate: finishDate ? new Date(finishDate) : undefined,
|
|
79
|
+
},
|
|
80
|
+
}, project, TreeStructureGroup.Iterations);
|
|
81
|
+
if (iteration) {
|
|
82
|
+
results.push(iteration);
|
|
149
83
|
}
|
|
150
|
-
const simplifiedResults = {
|
|
151
|
-
...rawResults,
|
|
152
|
-
teamMembers: (rawResults.teamMembers || []).map((member) => {
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
154
|
-
const { url, ...rest } = member;
|
|
155
|
-
return {
|
|
156
|
-
...rest,
|
|
157
|
-
teamMember: member.teamMember
|
|
158
|
-
? {
|
|
159
|
-
displayName: member.teamMember.displayName,
|
|
160
|
-
id: member.teamMember.id,
|
|
161
|
-
uniqueName: member.teamMember.uniqueName,
|
|
162
|
-
}
|
|
163
|
-
: undefined,
|
|
164
|
-
};
|
|
165
|
-
}),
|
|
166
|
-
};
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: JSON.stringify(simplifiedResults, null, 2) }],
|
|
169
|
-
};
|
|
170
84
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get iteration capacities for.");
|
|
174
|
-
if ("response" in result)
|
|
175
|
-
return result.response;
|
|
176
|
-
resolvedProject = result.resolved;
|
|
177
|
-
}
|
|
178
|
-
if (!iterationId) {
|
|
179
|
-
return { content: [{ type: "text", text: "iterationId is required for get_iteration_capacities" }], isError: true };
|
|
180
|
-
}
|
|
181
|
-
const workApi = await connection.getWorkApi();
|
|
182
|
-
const rawResults = await workApi.getTotalIterationCapacities(resolvedProject, iterationId);
|
|
183
|
-
if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
|
|
184
|
-
return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
content: [{ type: "text", text: JSON.stringify(rawResults, null, 2) }],
|
|
188
|
-
};
|
|
85
|
+
if (results.length === 0) {
|
|
86
|
+
return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
|
|
189
87
|
}
|
|
190
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
90
|
+
};
|
|
191
91
|
}
|
|
192
92
|
catch (error) {
|
|
193
93
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Error creating iterations: ${errorMessage}` }],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
server.tool(WORK_TOOLS.list_iterations, "List all iterations in a specified Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
101
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
102
|
+
depth: z.coerce.number().default(2).describe("Depth of children to fetch."),
|
|
103
|
+
excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and their children, that should not be returned."),
|
|
104
|
+
}, async ({ project, depth, excludedIds: ids }) => {
|
|
105
|
+
try {
|
|
106
|
+
const connection = await connectionProvider();
|
|
107
|
+
let resolvedProject = project;
|
|
108
|
+
if (!resolvedProject) {
|
|
109
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list iterations for.");
|
|
110
|
+
if ("response" in result)
|
|
111
|
+
return result.response;
|
|
112
|
+
resolvedProject = result.resolved;
|
|
113
|
+
}
|
|
114
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
115
|
+
let results = [];
|
|
116
|
+
if (depth === undefined) {
|
|
117
|
+
depth = 1;
|
|
118
|
+
}
|
|
119
|
+
results = await workItemTrackingApi.getClassificationNodes(resolvedProject, [], depth);
|
|
120
|
+
// Handle null or undefined results
|
|
121
|
+
if (!results) {
|
|
122
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
123
|
+
}
|
|
124
|
+
// Filter out items with structureType=0 (Area nodes), only keep structureType=1 (Iteration nodes)
|
|
125
|
+
let filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
|
|
126
|
+
// If specific IDs are provided, filter them out recursively (exclude matching nodes and their children)
|
|
127
|
+
if (ids && ids.length > 0) {
|
|
128
|
+
const filterOutIds = (nodes) => {
|
|
129
|
+
return nodes
|
|
130
|
+
.filter((node) => !node.id || !ids.includes(node.id))
|
|
131
|
+
.map((node) => {
|
|
132
|
+
if (node.children && node.children.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
...node,
|
|
135
|
+
children: filterOutIds(node.children),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return node;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
filteredResults = filterOutIds(filteredResults);
|
|
142
|
+
}
|
|
143
|
+
if (filteredResults.length === 0) {
|
|
144
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
|
|
200
148
|
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
201
152
|
return {
|
|
202
|
-
content: [{ type: "text", text:
|
|
153
|
+
content: [{ type: "text", text: `Error fetching iterations: ${errorMessage}` }],
|
|
203
154
|
isError: true,
|
|
204
155
|
};
|
|
205
156
|
}
|
|
206
157
|
});
|
|
207
|
-
server.tool(WORK_TOOLS.
|
|
208
|
-
action: z.enum(["create", "assign"]).describe("The action to perform. 'create' creates new iterations in the project; 'assign' assigns existing iterations to a team."),
|
|
158
|
+
server.tool(WORK_TOOLS.assign_iterations, "Assign existing iterations to a specific team in a project.", {
|
|
209
159
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
210
|
-
team: z.string().
|
|
160
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
211
161
|
iterations: z
|
|
212
162
|
.array(z.object({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
finishDate: z.string().optional().describe("The finish date of the iteration in ISO format (e.g., '2023-01-31T23:59:59Z'). Used for create."),
|
|
216
|
-
identifier: z.string().optional().describe("The identifier of the iteration to assign. Used for assign."),
|
|
217
|
-
path: z.string().optional().describe("The path of the iteration to assign, e.g., 'Project/Iteration'. Used for assign."),
|
|
163
|
+
identifier: z.string().describe("The identifier of the iteration to assign."),
|
|
164
|
+
path: z.string().describe("The path of the iteration to assign, e.g., 'Project/Iteration'."),
|
|
218
165
|
}))
|
|
219
|
-
.describe("An array of iterations to
|
|
220
|
-
}, async ({
|
|
166
|
+
.describe("An array of iterations to assign. Each iteration must have an identifier and a path."),
|
|
167
|
+
}, async ({ project, team, iterations }) => {
|
|
221
168
|
try {
|
|
222
169
|
const connection = await connectionProvider();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
name: iterationName,
|
|
231
|
-
attributes: {
|
|
232
|
-
startDate: startDate ? new Date(startDate) : undefined,
|
|
233
|
-
finishDate: finishDate ? new Date(finishDate) : undefined,
|
|
234
|
-
},
|
|
235
|
-
}, project, TreeStructureGroup.Iterations);
|
|
236
|
-
if (iteration) {
|
|
237
|
-
results.push(iteration);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (results.length === 0) {
|
|
241
|
-
return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
|
|
170
|
+
const workApi = await connection.getWorkApi();
|
|
171
|
+
const teamContext = { project, team };
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const { identifier, path } of iterations) {
|
|
174
|
+
const assignment = await workApi.postTeamIteration({ path: path, id: identifier }, teamContext);
|
|
175
|
+
if (assignment) {
|
|
176
|
+
results.push(assignment);
|
|
242
177
|
}
|
|
243
|
-
return {
|
|
244
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
245
|
-
};
|
|
246
178
|
}
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
return { content: [{ type: "text", text: "Team is required for assign" }], isError: true };
|
|
250
|
-
}
|
|
251
|
-
const workApi = await connection.getWorkApi();
|
|
252
|
-
const teamContext = { project, team };
|
|
253
|
-
const results = [];
|
|
254
|
-
for (const { identifier, path } of iterations) {
|
|
255
|
-
if (!identifier || !path)
|
|
256
|
-
continue;
|
|
257
|
-
const assignment = await workApi.postTeamIteration({ path: path, id: identifier }, teamContext);
|
|
258
|
-
if (assignment) {
|
|
259
|
-
results.push(assignment);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (results.length === 0) {
|
|
263
|
-
return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
267
|
-
};
|
|
179
|
+
if (results.length === 0) {
|
|
180
|
+
return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
|
|
268
181
|
}
|
|
269
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
184
|
+
};
|
|
270
185
|
}
|
|
271
186
|
catch (error) {
|
|
272
187
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: `Error assigning iterations: ${errorMessage}` }],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
server.tool(WORK_TOOLS.get_team_capacity, "Get the team capacity of a specific team and iteration in a project. If a project is not specified, you will be prompted to select one.", {
|
|
195
|
+
project: z.string().optional().describe("The name or Id of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
196
|
+
team: z.string().describe("The name or Id of the Azure DevOps team. Reuse from prior context if already known."),
|
|
197
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
198
|
+
}, async ({ project, team, iterationId }) => {
|
|
199
|
+
try {
|
|
200
|
+
const connection = await connectionProvider();
|
|
201
|
+
let resolvedProject = project;
|
|
202
|
+
if (!resolvedProject) {
|
|
203
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team capacity for.");
|
|
204
|
+
if ("response" in result)
|
|
205
|
+
return result.response;
|
|
206
|
+
resolvedProject = result.resolved;
|
|
207
|
+
}
|
|
208
|
+
const workApi = await connection.getWorkApi();
|
|
209
|
+
const teamContext = { project: resolvedProject, team };
|
|
210
|
+
const rawResults = await workApi.getCapacitiesWithIdentityRefAndTotals(teamContext, iterationId);
|
|
211
|
+
if (!rawResults || rawResults.teamMembers?.length === 0) {
|
|
212
|
+
return { content: [{ type: "text", text: "No team capacity assigned to the team" }], isError: true };
|
|
213
|
+
}
|
|
214
|
+
// Remove unwanted fields from teamMember and url
|
|
215
|
+
const simplifiedResults = {
|
|
216
|
+
...rawResults,
|
|
217
|
+
teamMembers: (rawResults.teamMembers || []).map((member) => {
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
219
|
+
const { url, ...rest } = member;
|
|
220
|
+
return {
|
|
221
|
+
...rest,
|
|
222
|
+
teamMember: member.teamMember
|
|
223
|
+
? {
|
|
224
|
+
displayName: member.teamMember.displayName,
|
|
225
|
+
id: member.teamMember.id,
|
|
226
|
+
uniqueName: member.teamMember.uniqueName,
|
|
227
|
+
}
|
|
228
|
+
: undefined,
|
|
229
|
+
};
|
|
230
|
+
}),
|
|
276
231
|
};
|
|
277
232
|
return {
|
|
278
|
-
content: [{ type: "text", text:
|
|
233
|
+
content: [{ type: "text", text: JSON.stringify(simplifiedResults, null, 2) }],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: `Error getting team capacity: ${errorMessage}` }],
|
|
279
240
|
isError: true,
|
|
280
241
|
};
|
|
281
242
|
}
|
|
282
243
|
});
|
|
283
|
-
server.tool(WORK_TOOLS.
|
|
284
|
-
action: z.literal("update").describe("The action to perform. Only 'update' is supported."),
|
|
244
|
+
server.tool(WORK_TOOLS.update_team_capacity, "Update the team capacity of a team member for a specific iteration in a project.", {
|
|
285
245
|
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
286
246
|
team: z.string().describe("The name or Id of the Azure DevOps team."),
|
|
287
247
|
teamMemberId: z.string().describe("The team member Id for the specific team member."),
|
|
@@ -304,6 +264,7 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
304
264
|
const connection = await connectionProvider();
|
|
305
265
|
const workApi = await connection.getWorkApi();
|
|
306
266
|
const teamContext = { project, team };
|
|
267
|
+
// Prepare the capacity update object
|
|
307
268
|
const capacityPatch = {
|
|
308
269
|
activities: activities.map((a) => ({
|
|
309
270
|
name: a.name,
|
|
@@ -314,10 +275,12 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
314
275
|
end: new Date(d.end),
|
|
315
276
|
})),
|
|
316
277
|
};
|
|
278
|
+
// Update the team member's capacity
|
|
317
279
|
const updatedCapacity = await workApi.updateCapacityWithIdentityRef(capacityPatch, teamContext, iterationId, teamMemberId);
|
|
318
280
|
if (!updatedCapacity) {
|
|
319
281
|
return { content: [{ type: "text", text: "Failed to update team member capacity" }], isError: true };
|
|
320
282
|
}
|
|
283
|
+
// Simplify output
|
|
321
284
|
const simplifiedResult = {
|
|
322
285
|
teamMember: updatedCapacity.teamMember
|
|
323
286
|
? {
|
|
@@ -341,5 +304,88 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
341
304
|
};
|
|
342
305
|
}
|
|
343
306
|
});
|
|
307
|
+
server.tool(WORK_TOOLS.get_iteration_capacities, "Get an iteration's capacity for all teams in iteration and project. If a project is not specified, you will be prompted to select one.", {
|
|
308
|
+
project: z.string().optional().describe("The name or Id of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
309
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
310
|
+
}, async ({ project, iterationId }) => {
|
|
311
|
+
try {
|
|
312
|
+
const connection = await connectionProvider();
|
|
313
|
+
let resolvedProject = project;
|
|
314
|
+
if (!resolvedProject) {
|
|
315
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get iteration capacities for.");
|
|
316
|
+
if ("response" in result)
|
|
317
|
+
return result.response;
|
|
318
|
+
resolvedProject = result.resolved;
|
|
319
|
+
}
|
|
320
|
+
const workApi = await connection.getWorkApi();
|
|
321
|
+
const rawResults = await workApi.getTotalIterationCapacities(resolvedProject, iterationId);
|
|
322
|
+
if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
|
|
323
|
+
return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: JSON.stringify(rawResults, null, 2) }],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: `Error getting iteration capacities: ${errorMessage}` }],
|
|
333
|
+
isError: true,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
server.tool(WORK_TOOLS.get_team_settings, "Get team settings including default iteration, backlog iteration, and default area path for a team. If a project or team is not specified, you will be prompted to select one.", {
|
|
338
|
+
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
339
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
340
|
+
}, async ({ project, team }) => {
|
|
341
|
+
try {
|
|
342
|
+
const connection = await connectionProvider();
|
|
343
|
+
let resolvedProject = project;
|
|
344
|
+
if (!resolvedProject) {
|
|
345
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team settings for.");
|
|
346
|
+
if ("response" in result)
|
|
347
|
+
return result.response;
|
|
348
|
+
resolvedProject = result.resolved;
|
|
349
|
+
}
|
|
350
|
+
let resolvedTeam = team;
|
|
351
|
+
if (!resolvedTeam) {
|
|
352
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to get settings for.");
|
|
353
|
+
if ("response" in result)
|
|
354
|
+
return result.response;
|
|
355
|
+
resolvedTeam = result.resolved;
|
|
356
|
+
}
|
|
357
|
+
const workApi = await connection.getWorkApi();
|
|
358
|
+
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
359
|
+
const teamSettings = await workApi.getTeamSettings(teamContext);
|
|
360
|
+
if (!teamSettings) {
|
|
361
|
+
return { content: [{ type: "text", text: "No team settings found" }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
const teamFieldValues = await workApi.getTeamFieldValues(teamContext);
|
|
364
|
+
const result = {
|
|
365
|
+
backlogIteration: teamSettings.backlogIteration,
|
|
366
|
+
defaultIteration: teamSettings.defaultIteration,
|
|
367
|
+
defaultIterationMacro: teamSettings.defaultIterationMacro,
|
|
368
|
+
backlogVisibilities: teamSettings.backlogVisibilities,
|
|
369
|
+
bugsBehavior: teamSettings.bugsBehavior,
|
|
370
|
+
workingDays: teamSettings.workingDays,
|
|
371
|
+
defaultAreaPath: teamFieldValues?.defaultValue,
|
|
372
|
+
areaPathField: teamFieldValues?.field,
|
|
373
|
+
areaPaths: teamFieldValues?.values,
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
378
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `Error fetching team settings: ${errorMessage}` }],
|
|
386
|
+
isError: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
344
390
|
}
|
|
345
391
|
export { WORK_TOOLS, configureWorkTools };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.8.1
|
|
1
|
+
export const packageVersion = "2.8.1";
|