@azure-devops/mcp 2.7.0-nightly.20260427 → 2.7.0-nightly.20260429
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/pipelines.js +31 -9
- package/dist/tools/repositories.js +22 -13
- package/dist/tools/test-plans.js +3 -1
- package/dist/tools/wiki.js +10 -3
- package/dist/tools/work-items.js +46 -6
- package/dist/utils.js +23 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools/pipelines.js
CHANGED
|
@@ -27,7 +27,10 @@ const PIPELINE_TOOLS = {
|
|
|
27
27
|
function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
28
28
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_definitions, "Retrieves a list of build definitions for a given project.", {
|
|
29
29
|
project: z.string().describe("Project ID or name to get build definitions for"),
|
|
30
|
-
repositoryId: z
|
|
30
|
+
repositoryId: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Repository ID to filter build definitions. Can be a GUID or a repository name; when a name is provided, it is auto-resolved to the repository GUID using the project parameter (Azure Repos / TfsGit only)."),
|
|
31
34
|
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter build definitions"),
|
|
32
35
|
name: z.string().optional().describe("Name of the build definition to filter"),
|
|
33
36
|
path: z.string().optional().describe("Path of the build definition to filter"),
|
|
@@ -49,7 +52,24 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
49
52
|
}, async ({ project, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
|
|
50
53
|
const connection = await connectionProvider();
|
|
51
54
|
const buildApi = await connection.getBuildApi();
|
|
52
|
-
|
|
55
|
+
// Auto-resolve repositoryId from name to GUID for Azure Repos
|
|
56
|
+
let resolvedRepositoryId = repositoryId;
|
|
57
|
+
if (repositoryId) {
|
|
58
|
+
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(repositoryId);
|
|
59
|
+
if (!isGuid && (!repositoryType || repositoryType === "TfsGit")) {
|
|
60
|
+
const gitApi = await connection.getGitApi();
|
|
61
|
+
const repositories = await gitApi.getRepositories(project);
|
|
62
|
+
const repo = repositories?.find((r) => r.name === repositoryId);
|
|
63
|
+
if (!repo?.id) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: `Error: Repository '${repositoryId}' not found in project '${project}'.` }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
resolvedRepositoryId = repo.id;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const buildDefinitions = await buildApi.getDefinitions(project, name, resolvedRepositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
|
|
53
73
|
return {
|
|
54
74
|
content: [{ type: "text", text: JSON.stringify(buildDefinitions, null, 2) }],
|
|
55
75
|
};
|
|
@@ -329,19 +349,21 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
329
349
|
content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }],
|
|
330
350
|
};
|
|
331
351
|
});
|
|
332
|
-
server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact.", {
|
|
352
|
+
server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact. When destinationPath is provided, it must be a relative local path; absolute paths and path traversal are not allowed.", {
|
|
333
353
|
project: z.string().describe("The name or ID of the project."),
|
|
334
354
|
buildId: z.coerce.number().min(1).describe("The ID of the build."),
|
|
335
355
|
artifactName: z.string().describe("The name of the artifact to download."),
|
|
336
|
-
destinationPath: z.string().optional().describe("The local path to download the artifact to. If not provided, returns binary content as base64."),
|
|
356
|
+
destinationPath: z.string().optional().describe("The relative local path to download the artifact to. If not provided, returns binary content as base64."),
|
|
337
357
|
}, async ({ project, buildId, artifactName, destinationPath }) => {
|
|
338
|
-
const
|
|
358
|
+
const hasUnsafePathSegment = (value) => value.split(/[\\/]+/).some((segment) => segment === "." || segment === "..");
|
|
359
|
+
const hasPathSeparators = (value) => /[\\/]/.test(value);
|
|
339
360
|
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
340
|
-
|
|
341
|
-
|
|
361
|
+
const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
|
|
362
|
+
if (hasUnsafePathSegment(artifactName) || hasPathSeparators(artifactName) || hasDriveLetter(artifactName) || isAbsolutePath(artifactName)) {
|
|
363
|
+
throw new Error("Invalid artifactName: artifactName must be a file name, not a path.");
|
|
342
364
|
}
|
|
343
|
-
if (destinationPath && (destinationPath
|
|
344
|
-
throw new Error("Invalid destinationPath:
|
|
365
|
+
if (destinationPath && (hasUnsafePathSegment(destinationPath) || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
|
|
366
|
+
throw new Error("Invalid destinationPath: use a relative path without path traversal.");
|
|
345
367
|
}
|
|
346
368
|
const connection = await connectionProvider();
|
|
347
369
|
const buildApi = await connection.getBuildApi();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlChangeType, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
|
|
6
|
-
import { getEnumKeys, streamToString } from "../utils.js";
|
|
6
|
+
import { extractAdoStreamError, getEnumKeys, streamToString } from "../utils.js";
|
|
7
7
|
const REPO_TOOLS = {
|
|
8
8
|
list_repos_by_project: "repo_list_repos_by_project",
|
|
9
9
|
list_pull_requests_by_repo_or_project: "repo_list_pull_requests_by_repo_or_project",
|
|
@@ -618,33 +618,33 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
618
618
|
try {
|
|
619
619
|
const connection = await connectionProvider();
|
|
620
620
|
const gitApi = await connection.getGitApi();
|
|
621
|
-
const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
|
|
621
|
+
const threads = (await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration)) ?? [];
|
|
622
622
|
let filteredThreads = threads;
|
|
623
623
|
if (status !== undefined) {
|
|
624
624
|
const statusValue = CommentThreadStatus[status];
|
|
625
|
-
filteredThreads = filteredThreads
|
|
625
|
+
filteredThreads = filteredThreads.filter((thread) => thread.status === statusValue);
|
|
626
626
|
}
|
|
627
627
|
if (authorEmail !== undefined) {
|
|
628
|
-
filteredThreads = filteredThreads
|
|
628
|
+
filteredThreads = filteredThreads.filter((thread) => {
|
|
629
629
|
const firstComment = thread.comments?.[0];
|
|
630
630
|
return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
|
|
631
631
|
});
|
|
632
632
|
}
|
|
633
633
|
if (authorDisplayName !== undefined) {
|
|
634
634
|
const lowerAuthorName = authorDisplayName.toLowerCase();
|
|
635
|
-
filteredThreads = filteredThreads
|
|
635
|
+
filteredThreads = filteredThreads.filter((thread) => {
|
|
636
636
|
const firstComment = thread.comments?.[0];
|
|
637
637
|
return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
|
|
638
638
|
});
|
|
639
639
|
}
|
|
640
|
-
const paginatedThreads = filteredThreads
|
|
640
|
+
const paginatedThreads = filteredThreads.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
|
|
641
641
|
if (fullResponse) {
|
|
642
642
|
return {
|
|
643
643
|
content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
|
|
644
644
|
};
|
|
645
645
|
}
|
|
646
646
|
// Return trimmed thread data focusing on essential information
|
|
647
|
-
const trimmedThreads = paginatedThreads
|
|
647
|
+
const trimmedThreads = paginatedThreads.map((thread) => trimPullRequestThread(thread));
|
|
648
648
|
return {
|
|
649
649
|
content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
|
|
650
650
|
};
|
|
@@ -693,7 +693,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
693
693
|
};
|
|
694
694
|
}
|
|
695
695
|
});
|
|
696
|
-
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of
|
|
696
|
+
server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branch names for a given repository. Returns an array of branch name strings, not full branch objects. Use repo_get_branch_by_name to get full details for a specific branch.", {
|
|
697
697
|
repositoryId: z
|
|
698
698
|
.string()
|
|
699
699
|
.describe("The ID or name of the repository where the branches are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
@@ -718,7 +718,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
718
718
|
};
|
|
719
719
|
}
|
|
720
720
|
});
|
|
721
|
-
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my
|
|
721
|
+
server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branch names for a given repository Id. Returns an array of branch name strings, not full branch objects. Use repo_get_branch_by_name to get full details for a specific branch.", {
|
|
722
722
|
repositoryId: z
|
|
723
723
|
.string()
|
|
724
724
|
.describe("The ID or name of the repository where the branches are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
@@ -770,7 +770,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
770
770
|
};
|
|
771
771
|
}
|
|
772
772
|
});
|
|
773
|
-
server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
|
|
773
|
+
server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name. Returns isError: true if the branch is not found.", {
|
|
774
774
|
repositoryId: z.string().describe("The ID or name of the repository where the branch is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
775
775
|
branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
|
|
776
776
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
@@ -1608,7 +1608,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1608
1608
|
],
|
|
1609
1609
|
};
|
|
1610
1610
|
});
|
|
1611
|
-
server.tool(REPO_TOOLS.list_directory, "List files and folders in a directory within a repository. Useful for exploring the structure of a codebase or finding related files.", {
|
|
1611
|
+
server.tool(REPO_TOOLS.list_directory, "List files and folders in a directory within a repository. Useful for exploring the structure of a codebase or finding related files. Returns isError: true if the path is not found.", {
|
|
1612
1612
|
repositoryId: z.string().describe("The ID or name of the repository."),
|
|
1613
1613
|
path: z.string().optional().default("/").describe("The directory path to list (e.g., '/src' or '/src/components'). Defaults to repository root."),
|
|
1614
1614
|
project: z.string().optional().describe("Project ID or name. Required if repositoryId is a name rather than a GUID."),
|
|
@@ -1629,7 +1629,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1629
1629
|
const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
|
|
1630
1630
|
if (!items || items.length === 0) {
|
|
1631
1631
|
return {
|
|
1632
|
-
content: [{ type: "text", text: `No items found at path: ${path}
|
|
1632
|
+
content: [{ type: "text", text: `No items found at path: ${path}. The path may not exist in the repository.` }],
|
|
1633
|
+
isError: true,
|
|
1633
1634
|
};
|
|
1634
1635
|
}
|
|
1635
1636
|
let filteredItems = items;
|
|
@@ -1677,7 +1678,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1677
1678
|
// ── Get file content at a specific version (branch, tag, or commit) ──
|
|
1678
1679
|
const fileVersionTypeStrings = getEnumKeys(GitVersionType);
|
|
1679
1680
|
server.tool(REPO_TOOLS.get_file_content, "Get the content of a file from a Git repository at a specific version (branch, tag, or commit SHA). " +
|
|
1680
|
-
"Useful for reading source files from PR branches, specific commits, or tags without having them checked out locally."
|
|
1681
|
+
"Useful for reading source files from PR branches, specific commits, or tags without having them checked out locally. " +
|
|
1682
|
+
"Returns isError: true if the file is not found.", {
|
|
1681
1683
|
repositoryId: z.string().describe("The ID (GUID) or name of the repository."),
|
|
1682
1684
|
path: z.string().describe("The full path to the file in the repository, e.g., '/src/main.ts' or 'src/main.ts'."),
|
|
1683
1685
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name."),
|
|
@@ -1710,6 +1712,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1710
1712
|
versionDescriptor, true // includeContent
|
|
1711
1713
|
);
|
|
1712
1714
|
const content = await streamToString(stream);
|
|
1715
|
+
const streamError = extractAdoStreamError(content);
|
|
1716
|
+
if (streamError) {
|
|
1717
|
+
return {
|
|
1718
|
+
content: [{ type: "text", text: `Error getting file content for '${path}': ${streamError}` }],
|
|
1719
|
+
isError: true,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1713
1722
|
return {
|
|
1714
1723
|
content: [{ type: "text", text: content }],
|
|
1715
1724
|
};
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -357,7 +357,9 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider, userA
|
|
|
357
357
|
if (testResultDetails.resultsForGroup) {
|
|
358
358
|
for (const group of testResultDetails.resultsForGroup) {
|
|
359
359
|
if (group.results) {
|
|
360
|
-
|
|
360
|
+
for (const result of group.results) {
|
|
361
|
+
allResults.push(result);
|
|
362
|
+
}
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
365
|
}
|
package/dist/tools/wiki.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { apiVersion } from "../utils.js";
|
|
4
|
+
import { apiVersion, extractAdoStreamError } from "../utils.js";
|
|
5
5
|
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
6
6
|
const WIKI_TOOLS = {
|
|
7
7
|
list_wikis: "wiki_list_wikis",
|
|
@@ -88,7 +88,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
|
-
server.tool(WIKI_TOOLS.get_wiki_page, "Retrieve wiki page metadata by path. This tool does not return page content.", {
|
|
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
92
|
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
93
93
|
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
94
94
|
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
@@ -136,7 +136,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
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.", {
|
|
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
140
|
url: z
|
|
141
141
|
.string()
|
|
142
142
|
.optional()
|
|
@@ -209,6 +209,13 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
209
209
|
return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
|
|
210
210
|
}
|
|
211
211
|
pageContent = await streamToString(stream);
|
|
212
|
+
const streamError = extractAdoStreamError(pageContent);
|
|
213
|
+
if (streamError) {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${streamError}` }],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
212
219
|
}
|
|
213
220
|
return createExternalContentResponse(pageContent, "wiki page");
|
|
214
221
|
}
|
package/dist/tools/work-items.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
3
5
|
import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
4
6
|
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
7
|
import { z } from "zod";
|
|
@@ -407,8 +409,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
407
409
|
if (revisions && Array.isArray(revisions)) {
|
|
408
410
|
revisions.forEach((revision) => {
|
|
409
411
|
if (revision.fields) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
const fields = revision.fields;
|
|
413
|
+
Object.keys(fields).forEach((fieldName) => {
|
|
414
|
+
const fieldValue = fields[fieldName];
|
|
412
415
|
// Check if this is an identity object by looking for common identity properties
|
|
413
416
|
if (fieldValue &&
|
|
414
417
|
typeof fieldValue === "object" &&
|
|
@@ -1210,11 +1213,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1210
1213
|
};
|
|
1211
1214
|
}
|
|
1212
1215
|
});
|
|
1213
|
-
server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID
|
|
1216
|
+
server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID. By default returns the content as a base64-encoded resource. If savePath is provided, saves the file locally to that directory and returns the file path instead. Useful for viewing images (e.g. screenshots) or other files attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
|
|
1214
1217
|
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."),
|
|
1215
1218
|
attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
|
|
1216
|
-
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type
|
|
1217
|
-
|
|
1219
|
+
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type or the saved file's name."),
|
|
1220
|
+
savePath: z
|
|
1221
|
+
.string()
|
|
1222
|
+
.optional()
|
|
1223
|
+
.describe("Optional local directory path where the file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments'); absolute paths and path traversals are not allowed. If provided, saves the attachment to this directory and returns the file path. If omitted, returns the content as a base64-encoded resource."),
|
|
1224
|
+
}, async ({ project, attachmentId, fileName, savePath }) => {
|
|
1225
|
+
const isAbsolutePath = (value) => path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
1226
|
+
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
1227
|
+
if (savePath !== undefined && (savePath.includes("..") || isAbsolutePath(savePath) || hasDriveLetter(savePath))) {
|
|
1228
|
+
throw new Error("Invalid savePath: absolute paths and path traversals are not allowed.");
|
|
1229
|
+
}
|
|
1230
|
+
if (fileName !== undefined && fileName.includes("..")) {
|
|
1231
|
+
throw new Error("Invalid fileName: path traversal is not allowed.");
|
|
1232
|
+
}
|
|
1218
1233
|
try {
|
|
1219
1234
|
const connection = await connectionProvider();
|
|
1220
1235
|
let resolvedProject = project;
|
|
@@ -1233,8 +1248,24 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1233
1248
|
stream.on("error", reject);
|
|
1234
1249
|
});
|
|
1235
1250
|
const buffer = Buffer.concat(chunks);
|
|
1236
|
-
|
|
1251
|
+
if (savePath) {
|
|
1252
|
+
const resolvedFileName = fileName ?? attachmentId;
|
|
1253
|
+
const localFilePath = path.join(savePath, resolvedFileName);
|
|
1254
|
+
if (fs.existsSync(localFilePath)) {
|
|
1255
|
+
throw new Error(`File already exists: ${localFilePath}`);
|
|
1256
|
+
}
|
|
1257
|
+
fs.writeFileSync(localFilePath, buffer);
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: `Attachment saved to: ${localFilePath}` }],
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1237
1262
|
const mimeType = getMimeType(fileName);
|
|
1263
|
+
if (mimeType.startsWith("text/")) {
|
|
1264
|
+
return {
|
|
1265
|
+
content: [{ type: "text", text: buffer.toString("utf-8") }],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const base64Data = buffer.toString("base64");
|
|
1238
1269
|
return {
|
|
1239
1270
|
content: [
|
|
1240
1271
|
{
|
|
@@ -1269,6 +1300,15 @@ function getMimeType(fileName) {
|
|
|
1269
1300
|
webp: "image/webp",
|
|
1270
1301
|
pdf: "application/pdf",
|
|
1271
1302
|
txt: "text/plain",
|
|
1303
|
+
md: "text/markdown",
|
|
1304
|
+
markdown: "text/markdown",
|
|
1305
|
+
csv: "text/csv",
|
|
1306
|
+
html: "text/html",
|
|
1307
|
+
htm: "text/html",
|
|
1308
|
+
xml: "text/xml",
|
|
1309
|
+
json: "application/json",
|
|
1310
|
+
yaml: "text/yaml",
|
|
1311
|
+
yml: "text/yaml",
|
|
1272
1312
|
zip: "application/zip",
|
|
1273
1313
|
};
|
|
1274
1314
|
return (ext && mimeTypes[ext]) ?? "application/octet-stream";
|
package/dist/utils.js
CHANGED
|
@@ -67,6 +67,29 @@ export function encodeFormattedValue(value, format) {
|
|
|
67
67
|
const result = value.replace(/</g, "<").replace(/>/g, ">");
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Detects whether a string returned from an ADO API stream is actually an error
|
|
72
|
+
* response serialized as JSON (e.g. a 404 GitItemNotFoundException or
|
|
73
|
+
* WikiPageNotFoundException) rather than real content.
|
|
74
|
+
*
|
|
75
|
+
* The ADO Node API client swallows non-2xx HTTP responses and delivers the
|
|
76
|
+
* error body as a stream, so callers must check explicitly after reading.
|
|
77
|
+
*
|
|
78
|
+
* @returns The human-readable error message extracted from the JSON, or null if
|
|
79
|
+
* the content is not an ADO error response.
|
|
80
|
+
*/
|
|
81
|
+
export function extractAdoStreamError(content) {
|
|
82
|
+
try {
|
|
83
|
+
const json = JSON.parse(content.trim());
|
|
84
|
+
if (json && typeof json.typeName === "string" && typeof json.message === "string") {
|
|
85
|
+
return json.message;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not JSON — not an ADO error response.
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
70
93
|
/**
|
|
71
94
|
* Convert a Node.js ReadableStream to a string.
|
|
72
95
|
* Shared utility for consistent stream handling across tools.
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.7.0-nightly.
|
|
1
|
+
export const packageVersion = "2.7.0-nightly.20260429";
|