@azure-devops/mcp 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -7
- package/dist/shared/command.js +34 -0
- package/dist/tools/advanced-security.js +31 -11
- package/dist/tools/pipelines.dto.js +95 -0
- package/dist/tools/pipelines.js +350 -373
- package/dist/tools/repositories.js +695 -1485
- package/dist/tools/test-plans.js +297 -396
- package/dist/tools/wiki.js +242 -273
- package/dist/tools/work-items.js +851 -1174
- package/dist/tools/work.js +239 -285
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools/wiki.js
CHANGED
|
@@ -4,247 +4,221 @@ 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
|
-
|
|
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",
|
|
7
|
+
wiki: "wiki",
|
|
8
|
+
wiki_upsert_page: "wiki_upsert_page",
|
|
13
9
|
};
|
|
14
10
|
function configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
15
|
-
server.tool(WIKI_TOOLS.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
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')."),
|
|
11
|
+
server.tool(WIKI_TOOLS.wiki, "Retrieve wiki data for an organization or project. Use the action parameter to specify the operation.", {
|
|
12
|
+
action: z
|
|
13
|
+
.enum(["list_wikis", "get_wiki", "list_pages", "get_page", "get_page_content"])
|
|
14
|
+
.describe("The action to perform. Options: list_wikis (list all wikis in an organization or project), get_wiki (get details of a specific wiki), list_pages (list pages in a wiki), get_page (get wiki page metadata without content), get_page_content (retrieve wiki page content)."),
|
|
15
|
+
wikiIdentifier: z.string().optional().describe("The unique identifier of the wiki. Required for get_wiki, list_pages, get_page, and get_page_content (unless url is provided)."),
|
|
16
|
+
project: z.string().optional().describe("The project name or ID. Required for list_pages and get_page. Optional for list_wikis, get_wiki, and get_page_content."),
|
|
17
|
+
path: z.string().optional().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup'). Required for get_page. Optional for get_page_content."),
|
|
18
|
+
url: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("The full URL of the wiki page. Used for get_page_content. 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"),
|
|
22
|
+
top: z.coerce.number().default(20).describe("The maximum number of pages to return. Used for list_pages. Defaults to 20."),
|
|
23
|
+
continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages. Used for list_pages."),
|
|
24
|
+
pageViewsForDays: z.coerce.number().optional().describe("Number of days to retrieve page views for. Used for list_pages. If not specified, page views are not included."),
|
|
95
25
|
recursionLevel: z
|
|
96
26
|
.enum(["None", "OneLevel", "OneLevelPlusNestedEmptyFolders", "Full"])
|
|
97
27
|
.optional()
|
|
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 }) => {
|
|
28
|
+
.describe("Recursion level for subpages. Used for get_page. 'None' returns only the specified page. 'OneLevel' includes direct children. 'Full' includes all descendants."),
|
|
29
|
+
}, async ({ action, wikiIdentifier, project, path, url, top = 20, continuationToken, pageViewsForDays, recursionLevel }) => {
|
|
100
30
|
try {
|
|
101
31
|
const connection = await connectionProvider();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
if (recursionLevel) {
|
|
113
|
-
params.append("recursionLevel", recursionLevel);
|
|
114
|
-
}
|
|
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}`);
|
|
32
|
+
if (action === "list_wikis") {
|
|
33
|
+
const wikiApi = await connection.getWikiApi();
|
|
34
|
+
const wikis = await wikiApi.getAllWikis(project);
|
|
35
|
+
if (!wikis) {
|
|
36
|
+
return { content: [{ type: "text", text: "No wikis found" }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: JSON.stringify(wikis, null, 2) }],
|
|
40
|
+
};
|
|
125
41
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 };
|
|
42
|
+
if (action === "get_wiki") {
|
|
43
|
+
if (!wikiIdentifier) {
|
|
44
|
+
return { content: [{ type: "text", text: "wikiIdentifier is required for get_wiki" }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
const wikiApi = await connection.getWikiApi();
|
|
47
|
+
const wiki = await wikiApi.getWiki(wikiIdentifier, project);
|
|
48
|
+
if (!wiki) {
|
|
49
|
+
return { content: [{ type: "text", text: "No wiki found" }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(wiki, null, 2) }],
|
|
53
|
+
};
|
|
153
54
|
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
55
|
+
if (action === "list_pages") {
|
|
56
|
+
if (!wikiIdentifier) {
|
|
57
|
+
return { content: [{ type: "text", text: "wikiIdentifier is required for list_pages" }], isError: true };
|
|
58
|
+
}
|
|
59
|
+
if (!project) {
|
|
60
|
+
return { content: [{ type: "text", text: "project is required for list_pages" }], isError: true };
|
|
61
|
+
}
|
|
62
|
+
const wikiApi = await connection.getWikiApi();
|
|
63
|
+
const pagesBatchRequest = {
|
|
64
|
+
top,
|
|
65
|
+
continuationToken,
|
|
66
|
+
pageViewsForDays,
|
|
67
|
+
};
|
|
68
|
+
const pages = await wikiApi.getPagesBatch(pagesBatchRequest, project, wikiIdentifier);
|
|
69
|
+
if (!pages) {
|
|
70
|
+
return { content: [{ type: "text", text: "No wiki pages found" }], isError: true };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify(pages, null, 2) }],
|
|
74
|
+
};
|
|
156
75
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 };
|
|
76
|
+
if (action === "get_page") {
|
|
77
|
+
if (!wikiIdentifier) {
|
|
78
|
+
return { content: [{ type: "text", text: "wikiIdentifier is required for get_page" }], isError: true };
|
|
167
79
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
};
|
|
80
|
+
if (!project) {
|
|
81
|
+
return { content: [{ type: "text", text: "project is required for get_page" }], isError: true };
|
|
183
82
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (parsed.pagePath) {
|
|
187
|
-
resolvedPath = parsed.pagePath;
|
|
83
|
+
if (!path) {
|
|
84
|
+
return { content: [{ type: "text", text: "path is required for get_page" }], isError: true };
|
|
188
85
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (action === "get_page_content") {
|
|
115
|
+
const hasUrl = !!url;
|
|
116
|
+
const hasPair = !!wikiIdentifier && !!project;
|
|
117
|
+
if (hasUrl && hasPair) {
|
|
118
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: Provide either 'url' OR 'wikiIdentifier' with 'project', not both." }], isError: true };
|
|
119
|
+
}
|
|
120
|
+
if (!hasUrl && !hasPair) {
|
|
121
|
+
return { content: [{ type: "text", text: "Error fetching wiki page content: You must provide either 'url' OR both 'wikiIdentifier' and 'project'." }], isError: true };
|
|
122
|
+
}
|
|
123
|
+
const wikiApi = await connection.getWikiApi();
|
|
124
|
+
let resolvedProject = project;
|
|
125
|
+
let resolvedWiki = wikiIdentifier;
|
|
126
|
+
let resolvedPath = path;
|
|
127
|
+
let pageContent;
|
|
128
|
+
if (url) {
|
|
129
|
+
const parsed = parseWikiUrl(url);
|
|
130
|
+
if ("error" in parsed) {
|
|
131
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: ${parsed.error}` }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
// Guard against cross-organization requests: a user-supplied URL must target the
|
|
134
|
+
// same organization the server is connected to. Otherwise the org segment in the
|
|
135
|
+
// URL would be silently ignored and content fetched from the configured org instead.
|
|
136
|
+
const configuredOrg = getOrgFromUrl(connection.serverUrl);
|
|
137
|
+
const urlOrg = getOrgFromUrl(url);
|
|
138
|
+
if (configuredOrg && urlOrg !== configuredOrg) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
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
|
+
}
|
|
204
173
|
}
|
|
205
|
-
else if (
|
|
206
|
-
|
|
174
|
+
else if (resp.status === 404) {
|
|
175
|
+
return { content: [{ type: "text", text: `Error fetching wiki page content: Page with id ${parsed.pageId} not found` }], isError: true };
|
|
207
176
|
}
|
|
208
177
|
}
|
|
209
|
-
|
|
210
|
-
return { content: [{ type: "text", text: `Error fetching wiki page content: Page with id ${parsed.pageId} not found` }], isError: true };
|
|
211
|
-
}
|
|
178
|
+
catch { }
|
|
212
179
|
}
|
|
213
|
-
catch { }
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (!pageContent) {
|
|
217
|
-
if (!resolvedPath) {
|
|
218
|
-
resolvedPath = "/";
|
|
219
180
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
181
|
+
if (!pageContent) {
|
|
182
|
+
if (!resolvedPath) {
|
|
183
|
+
resolvedPath = "/";
|
|
184
|
+
}
|
|
185
|
+
// resolvedProject and resolvedWiki are guaranteed to be defined here:
|
|
186
|
+
// - the url branch errors out in parseWikiUrl when project/wikiIdentifier are missing
|
|
187
|
+
// - the pair branch enforces both via the hasPair check above
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
189
|
+
const stream = await wikiApi.getPageText(resolvedProject, resolvedWiki, resolvedPath, undefined, undefined, true);
|
|
190
|
+
if (!stream) {
|
|
191
|
+
return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
|
|
192
|
+
}
|
|
193
|
+
pageContent = await streamToString(stream);
|
|
194
|
+
const streamError = extractAdoStreamError(pageContent);
|
|
195
|
+
if (streamError) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${streamError}` }],
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
235
201
|
}
|
|
202
|
+
return createExternalContentResponse(pageContent, "wiki page");
|
|
236
203
|
}
|
|
237
|
-
return
|
|
204
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
238
205
|
}
|
|
239
206
|
catch (error) {
|
|
240
207
|
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
|
+
};
|
|
241
215
|
return {
|
|
242
|
-
content: [{ type: "text", text: `Error
|
|
216
|
+
content: [{ type: "text", text: actionErrorMessages[action] ?? `Error: ${errorMessage}` }],
|
|
243
217
|
isError: true,
|
|
244
218
|
};
|
|
245
219
|
}
|
|
246
220
|
});
|
|
247
|
-
server.tool(WIKI_TOOLS.
|
|
221
|
+
server.tool(WIKI_TOOLS.wiki_upsert_page, "Create or update a wiki page with content.", {
|
|
248
222
|
wikiIdentifier: z.string().describe("The unique identifier or name of the wiki."),
|
|
249
223
|
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
250
224
|
content: z.string().describe("The content of the wiki page in markdown format."),
|
|
@@ -263,85 +237,80 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
263
237
|
const projectParam = project || "";
|
|
264
238
|
const url = `${baseUrl}/${encodeURIComponent(projectParam)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
|
|
265
239
|
// First, try to create a new page (PUT without ETag)
|
|
266
|
-
|
|
267
|
-
|
|
240
|
+
const createResponse = await fetch(url, {
|
|
241
|
+
method: "PUT",
|
|
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, {
|
|
268
286
|
method: "PUT",
|
|
269
287
|
headers: {
|
|
270
288
|
"Authorization": `Bearer ${accessToken}`,
|
|
271
289
|
"Content-Type": "application/json",
|
|
272
290
|
"User-Agent": userAgentProvider(),
|
|
291
|
+
"If-Match": currentEtag,
|
|
273
292
|
},
|
|
274
293
|
body: JSON.stringify({ content: content }),
|
|
275
294
|
});
|
|
276
|
-
if (
|
|
277
|
-
const result = await
|
|
295
|
+
if (updateResponse.ok) {
|
|
296
|
+
const result = await updateResponse.json();
|
|
278
297
|
return {
|
|
279
298
|
content: [
|
|
280
299
|
{
|
|
281
300
|
type: "text",
|
|
282
|
-
text: `Successfully
|
|
301
|
+
text: `Successfully updated wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
|
|
283
302
|
},
|
|
284
303
|
],
|
|
285
304
|
};
|
|
286
305
|
}
|
|
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
|
-
}
|
|
338
306
|
else {
|
|
339
|
-
const errorText = await
|
|
340
|
-
throw new Error(`Failed to
|
|
307
|
+
const errorText = await updateResponse.text();
|
|
308
|
+
throw new Error(`Failed to update page (${updateResponse.status}): ${errorText}`);
|
|
341
309
|
}
|
|
342
310
|
}
|
|
343
|
-
|
|
344
|
-
|
|
311
|
+
else {
|
|
312
|
+
const errorText = await createResponse.text();
|
|
313
|
+
throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`);
|
|
345
314
|
}
|
|
346
315
|
}
|
|
347
316
|
catch (error) {
|