@azure-devops/mcp 2.5.0-nightly.20260323 → 2.5.0-nightly.20260324
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/advanced-security.js +2 -2
- package/dist/tools/core.js +5 -5
- package/dist/tools/pipelines.js +25 -24
- package/dist/tools/repositories.js +405 -30
- package/dist/tools/search.js +6 -6
- package/dist/tools/test-plans.js +11 -11
- package/dist/tools/wiki.js +5 -5
- package/dist/tools/work-items.js +27 -27
- package/dist/tools/work.js +2 -2
- package/dist/utils.js +15 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ function configureAdvSecTools(server, _, connectionProvider) {
|
|
|
37
37
|
.array(z.enum(getEnumKeys(AlertValidityStatus)))
|
|
38
38
|
.optional()
|
|
39
39
|
.describe("Filter alerts by validity status. Only applicable for secret alerts."),
|
|
40
|
-
top: z.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
|
|
40
|
+
top: z.coerce.number().optional().default(100).describe("Maximum number of alerts to return. Defaults to 100."),
|
|
41
41
|
orderBy: z.enum(["id", "firstSeen", "lastSeen", "fixedOn", "severity"]).optional().default("severity").describe("Order results by specified field. Defaults to 'severity'."),
|
|
42
42
|
continuationToken: z.string().optional().describe("Continuation token for pagination."),
|
|
43
43
|
}, async ({ project, repository, alertType, states, severities, ruleId, ruleName, toolName, ref, onlyDefaultBranch, confidenceLevels, validity, top, orderBy, continuationToken }) => {
|
|
@@ -79,7 +79,7 @@ function configureAdvSecTools(server, _, connectionProvider) {
|
|
|
79
79
|
server.tool(ADVSEC_TOOLS.get_alert_details, "Get detailed information about a specific Advanced Security alert.", {
|
|
80
80
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
81
81
|
repository: z.string().describe("The name or ID of the repository containing the alert."),
|
|
82
|
-
alertId: z.number().describe("The ID of the alert to retrieve details for."),
|
|
82
|
+
alertId: z.coerce.number().min(1).describe("The ID of the alert to retrieve details for."),
|
|
83
83
|
ref: z.string().optional().describe("Git reference (branch) to filter the alert."),
|
|
84
84
|
}, async ({ project, repository, alertId, ref }) => {
|
|
85
85
|
try {
|
package/dist/tools/core.js
CHANGED
|
@@ -16,8 +16,8 @@ function configureCoreTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
16
16
|
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for an Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
17
17
|
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."),
|
|
18
18
|
mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
|
|
19
|
-
top: z.number().optional().describe("The maximum number of teams to return. Defaults to 100."),
|
|
20
|
-
skip: z.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
|
|
19
|
+
top: z.coerce.number().optional().describe("The maximum number of teams to return. Defaults to 100."),
|
|
20
|
+
skip: z.coerce.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
|
|
21
21
|
}, async ({ project, mine, top, skip }) => {
|
|
22
22
|
try {
|
|
23
23
|
const connection = await connectionProvider();
|
|
@@ -47,9 +47,9 @@ function configureCoreTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
47
47
|
});
|
|
48
48
|
server.tool(CORE_TOOLS.list_projects, "Retrieve a list of projects in your Azure DevOps organization.", {
|
|
49
49
|
stateFilter: z.enum(["all", "wellFormed", "createPending", "deleted"]).default("wellFormed").describe("Filter projects by their state. Defaults to 'wellFormed'."),
|
|
50
|
-
top: z.number().optional().describe("The maximum number of projects to return. Defaults to 100."),
|
|
51
|
-
skip: z.number().optional().describe("The number of projects to skip for pagination. Defaults to 0."),
|
|
52
|
-
continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
|
|
50
|
+
top: z.coerce.number().optional().describe("The maximum number of projects to return. Defaults to 100."),
|
|
51
|
+
skip: z.coerce.number().optional().describe("The number of projects to skip for pagination. Defaults to 0."),
|
|
52
|
+
continuationToken: z.coerce.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
|
|
53
53
|
projectNameFilter: z.string().optional().describe("Filter projects by name. Supports partial matches."),
|
|
54
54
|
}, async ({ stateFilter, top, skip, continuationToken, projectNameFilter }) => {
|
|
55
55
|
try {
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -37,7 +37,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
37
37
|
top: z.number().optional().describe("Maximum number of build definitions to return"),
|
|
38
38
|
continuationToken: z.string().optional().describe("Token for continuing paged results"),
|
|
39
39
|
minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter build definitions"),
|
|
40
|
-
definitionIds: z.array(z.number()).optional().describe("Array of build definition IDs to filter"),
|
|
40
|
+
definitionIds: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter"),
|
|
41
41
|
builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date"),
|
|
42
42
|
notBuiltAfter: z.coerce.date().optional().describe("Return definitions that do not have builds after this date"),
|
|
43
43
|
includeAllProperties: z.boolean().optional().describe("Whether to include all properties in the results"),
|
|
@@ -105,7 +105,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
105
105
|
});
|
|
106
106
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_definition_revisions, "Retrieves a list of revisions for a specific build definition.", {
|
|
107
107
|
project: z.string().describe("Project ID or name to get the build definition revisions for"),
|
|
108
|
-
definitionId: z.number().describe("ID of the build definition to get revisions for"),
|
|
108
|
+
definitionId: z.coerce.number().min(1).describe("ID of the build definition to get revisions for"),
|
|
109
109
|
}, async ({ project, definitionId }) => {
|
|
110
110
|
const connection = await connectionProvider();
|
|
111
111
|
const buildApi = await connection.getBuildApi();
|
|
@@ -116,8 +116,8 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
116
116
|
});
|
|
117
117
|
server.tool(PIPELINE_TOOLS.pipelines_get_builds, "Retrieves a list of builds for a given project.", {
|
|
118
118
|
project: z.string().describe("Project ID or name to get builds for"),
|
|
119
|
-
definitions: z.array(z.number()).optional().describe("Array of build definition IDs to filter builds"),
|
|
120
|
-
queues: z.array(z.number()).optional().describe("Array of queue IDs to filter builds"),
|
|
119
|
+
definitions: z.array(z.coerce.number().min(1)).optional().describe("Array of build definition IDs to filter builds"),
|
|
120
|
+
queues: z.array(z.coerce.number().min(1)).optional().describe("Array of queue IDs to filter builds"),
|
|
121
121
|
buildNumber: z.string().optional().describe("Build number to filter builds"),
|
|
122
122
|
minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds"),
|
|
123
123
|
maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds"),
|
|
@@ -137,7 +137,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
137
137
|
.optional()
|
|
138
138
|
.describe("Order in which builds are returned"),
|
|
139
139
|
branchName: z.string().optional().describe("Branch name to filter builds"),
|
|
140
|
-
buildIds: z.array(z.number()).optional().describe("Array of build IDs to retrieve"),
|
|
140
|
+
buildIds: z.array(z.coerce.number().min(1)).optional().describe("Array of build IDs to retrieve"),
|
|
141
141
|
repositoryId: z.string().optional().describe("Repository ID to filter builds"),
|
|
142
142
|
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter builds"),
|
|
143
143
|
}, async ({ project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType, }) => {
|
|
@@ -150,7 +150,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
150
150
|
});
|
|
151
151
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_log, "Retrieves the logs for a specific build.", {
|
|
152
152
|
project: z.string().describe("Project ID or name to get the build log for"),
|
|
153
|
-
buildId: z.number().describe("ID of the build to get the log for"),
|
|
153
|
+
buildId: z.coerce.number().min(1).describe("ID of the build to get the log for"),
|
|
154
154
|
}, async ({ project, buildId }) => {
|
|
155
155
|
const connection = await connectionProvider();
|
|
156
156
|
const buildApi = await connection.getBuildApi();
|
|
@@ -161,10 +161,10 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
161
161
|
});
|
|
162
162
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_log_by_id, "Get a specific build log by log ID.", {
|
|
163
163
|
project: z.string().describe("Project ID or name to get the build log for"),
|
|
164
|
-
buildId: z.number().describe("ID of the build to get the log for"),
|
|
165
|
-
logId: z.number().describe("ID of the log to retrieve"),
|
|
166
|
-
startLine: z.number().optional().describe("Starting line number for the log content, defaults to 0"),
|
|
167
|
-
endLine: z.number().optional().describe("Ending line number for the log content, defaults to the end of the log"),
|
|
164
|
+
buildId: z.coerce.number().min(1).describe("ID of the build to get the log for"),
|
|
165
|
+
logId: z.coerce.number().min(1).describe("ID of the log to retrieve"),
|
|
166
|
+
startLine: z.coerce.number().optional().describe("Starting line number for the log content, defaults to 0"),
|
|
167
|
+
endLine: z.coerce.number().optional().describe("Ending line number for the log content, defaults to the end of the log"),
|
|
168
168
|
}, async ({ project, buildId, logId, startLine, endLine }) => {
|
|
169
169
|
const connection = await connectionProvider();
|
|
170
170
|
const buildApi = await connection.getBuildApi();
|
|
@@ -175,7 +175,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
175
175
|
});
|
|
176
176
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_changes, "Get the changes associated with a specific build.", {
|
|
177
177
|
project: z.string().describe("Project ID or name to get the build changes for"),
|
|
178
|
-
buildId: z.number().describe("ID of the build to get changes for"),
|
|
178
|
+
buildId: z.coerce.number().min(1).describe("ID of the build to get changes for"),
|
|
179
179
|
continuationToken: z.string().optional().describe("Continuation token for pagination"),
|
|
180
180
|
top: z.number().default(100).describe("Number of changes to retrieve, defaults to 100"),
|
|
181
181
|
includeSourceChange: z.boolean().optional().describe("Whether to include source changes in the results, defaults to false"),
|
|
@@ -189,8 +189,8 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
189
189
|
});
|
|
190
190
|
server.tool(PIPELINE_TOOLS.pipelines_get_run, "Gets a run for a particular pipeline.", {
|
|
191
191
|
project: z.string().describe("Project ID or name to run the build in"),
|
|
192
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
193
|
-
runId: z.number().describe("ID of the run to get"),
|
|
192
|
+
pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
|
|
193
|
+
runId: z.coerce.number().min(1).describe("ID of the run to get"),
|
|
194
194
|
}, async ({ project, pipelineId, runId }) => {
|
|
195
195
|
const connection = await connectionProvider();
|
|
196
196
|
const pipelinesApi = await connection.getPipelinesApi();
|
|
@@ -201,7 +201,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
201
201
|
});
|
|
202
202
|
server.tool(PIPELINE_TOOLS.pipelines_list_runs, "Gets top 10000 runs for a particular pipeline.", {
|
|
203
203
|
project: z.string().describe("Project ID or name to run the build in"),
|
|
204
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
204
|
+
pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
|
|
205
205
|
}, async ({ project, pipelineId }) => {
|
|
206
206
|
const connection = await connectionProvider();
|
|
207
207
|
const pipelinesApi = await connection.getPipelinesApi();
|
|
@@ -227,7 +227,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
227
227
|
}))
|
|
228
228
|
.optional(),
|
|
229
229
|
pipelines: z.record(z.string().describe("Name of the pipeline resource."), z.object({
|
|
230
|
-
runId: z.number().describe("Id of the source pipeline run that triggered or is referenced by this pipeline run."),
|
|
230
|
+
runId: z.coerce.number().min(1).describe("Id of the source pipeline run that triggered or is referenced by this pipeline run."),
|
|
231
231
|
version: z.string().optional().describe("Version of the source pipeline run."),
|
|
232
232
|
})),
|
|
233
233
|
repositories: z
|
|
@@ -241,8 +241,8 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
241
241
|
});
|
|
242
242
|
server.tool(PIPELINE_TOOLS.pipelines_run_pipeline, "Starts a new run of a pipeline.", {
|
|
243
243
|
project: z.string().describe("Project ID or name to run the build in"),
|
|
244
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
245
|
-
pipelineVersion: z.number().optional().describe("Version of the pipeline to run. If not provided, the latest version will be used."),
|
|
244
|
+
pipelineId: z.coerce.number().min(1).describe("ID of the pipeline to run"),
|
|
245
|
+
pipelineVersion: z.coerce.number().min(1).optional().describe("Version of the pipeline to run. If not provided, the latest version will be used."),
|
|
246
246
|
previewRun: z.boolean().optional().describe("If true, returns the final YAML document after parsing templates without creating a new run."),
|
|
247
247
|
resources: resourcesSchema.optional().describe("A dictionary of resources to pass to the pipeline."),
|
|
248
248
|
stagesToSkip: z.array(z.string()).optional().describe("A list of stages to skip."),
|
|
@@ -277,7 +277,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
277
277
|
});
|
|
278
278
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_status, "Fetches the status of a specific build.", {
|
|
279
279
|
project: z.string().describe("Project ID or name to get the build status for"),
|
|
280
|
-
buildId: z.number().describe("ID of the build to get the status for"),
|
|
280
|
+
buildId: z.coerce.number().min(1).describe("ID of the build to get the status for"),
|
|
281
281
|
}, async ({ project, buildId }) => {
|
|
282
282
|
const connection = await connectionProvider();
|
|
283
283
|
const buildApi = await connection.getBuildApi();
|
|
@@ -288,14 +288,14 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
288
288
|
});
|
|
289
289
|
server.tool(PIPELINE_TOOLS.pipelines_update_build_stage, "Updates the stage of a specific build.", {
|
|
290
290
|
project: z.string().describe("Project ID or name to update the build stage for"),
|
|
291
|
-
buildId: z.number().describe("ID of the build to update"),
|
|
291
|
+
buildId: z.coerce.number().min(1).describe("ID of the build to update"),
|
|
292
292
|
stageName: z.string().describe("Name of the stage to update"),
|
|
293
293
|
status: z.enum(getEnumKeys(StageUpdateType)).describe("New status for the stage"),
|
|
294
294
|
forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage."),
|
|
295
295
|
}, async ({ project, buildId, stageName, status, forceRetryAllJobs }) => {
|
|
296
296
|
const connection = await connectionProvider();
|
|
297
297
|
const orgUrl = connection.serverUrl;
|
|
298
|
-
const endpoint = `${orgUrl}/${project}/_apis/build/builds/${buildId}/stages/${stageName}?api-version=${apiVersion}`;
|
|
298
|
+
const endpoint = `${orgUrl}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/stages/${encodeURIComponent(stageName)}?api-version=${apiVersion}`;
|
|
299
299
|
const token = await tokenProvider();
|
|
300
300
|
const body = {
|
|
301
301
|
forceRetryAllJobs: forceRetryAllJobs,
|
|
@@ -321,7 +321,7 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
321
321
|
});
|
|
322
322
|
server.tool(PIPELINE_TOOLS.pipelines_list_artifacts, "Lists artifacts for a given build.", {
|
|
323
323
|
project: z.string().describe("The name or ID of the project."),
|
|
324
|
-
buildId: z.number().describe("The ID of the build."),
|
|
324
|
+
buildId: z.coerce.number().min(1).describe("The ID of the build."),
|
|
325
325
|
}, async ({ project, buildId }) => {
|
|
326
326
|
const connection = await connectionProvider();
|
|
327
327
|
const buildApi = await connection.getBuildApi();
|
|
@@ -332,16 +332,17 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
332
332
|
});
|
|
333
333
|
server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact.", {
|
|
334
334
|
project: z.string().describe("The name or ID of the project."),
|
|
335
|
-
buildId: z.number().describe("The ID of the build."),
|
|
335
|
+
buildId: z.coerce.number().min(1).describe("The ID of the build."),
|
|
336
336
|
artifactName: z.string().describe("The name of the artifact to download."),
|
|
337
337
|
destinationPath: z.string().optional().describe("The local path to download the artifact to. If not provided, returns binary content as base64."),
|
|
338
338
|
}, async ({ project, buildId, artifactName, destinationPath }) => {
|
|
339
339
|
const isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
|
|
340
|
+
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
340
341
|
if (artifactName.includes("..")) {
|
|
341
342
|
throw new Error("Invalid artifactName: path traversal is not allowed.");
|
|
342
343
|
}
|
|
343
|
-
if (destinationPath && (destinationPath.includes("..") || isAbsolutePath(destinationPath))) {
|
|
344
|
-
throw new Error("Invalid destinationPath: absolute paths and
|
|
344
|
+
if (destinationPath && (destinationPath.includes("..") || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
|
|
345
|
+
throw new Error("Invalid destinationPath: absolute paths and path traversals are not allowed.");
|
|
345
346
|
}
|
|
346
347
|
const connection = await connectionProvider();
|
|
347
348
|
const buildApi = await connection.getBuildApi();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
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 } from "../utils.js";
|
|
6
|
+
import { 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",
|
|
@@ -14,6 +14,7 @@ const REPO_TOOLS = {
|
|
|
14
14
|
get_repo_by_name_or_id: "repo_get_repo_by_name_or_id",
|
|
15
15
|
get_branch_by_name: "repo_get_branch_by_name",
|
|
16
16
|
get_pull_request_by_id: "repo_get_pull_request_by_id",
|
|
17
|
+
get_pull_request_changes: "repo_get_pull_request_changes",
|
|
17
18
|
create_pull_request: "repo_create_pull_request",
|
|
18
19
|
create_branch: "repo_create_branch",
|
|
19
20
|
update_pull_request: "repo_update_pull_request",
|
|
@@ -25,6 +26,7 @@ const REPO_TOOLS = {
|
|
|
25
26
|
list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
|
|
26
27
|
vote_pull_request: "repo_vote_pull_request",
|
|
27
28
|
list_directory: "repo_list_directory",
|
|
29
|
+
get_file_content: "repo_get_file_content",
|
|
28
30
|
};
|
|
29
31
|
function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
30
32
|
return branches
|
|
@@ -289,7 +291,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
289
291
|
});
|
|
290
292
|
server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields, including setting autocomplete with various completion options.", {
|
|
291
293
|
repositoryId: z.string().describe("The ID or name of the repository where the pull request exists. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
292
|
-
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
294
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to update."),
|
|
293
295
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
294
296
|
title: z.string().optional().describe("The new title for the pull request."),
|
|
295
297
|
description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
|
|
@@ -392,7 +394,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
392
394
|
});
|
|
393
395
|
server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
|
|
394
396
|
repositoryId: z.string().describe("The ID or name of the repository where the pull request exists. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
395
|
-
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
397
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to update."),
|
|
396
398
|
reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
|
|
397
399
|
action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
|
|
398
400
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
@@ -434,8 +436,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
434
436
|
});
|
|
435
437
|
server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
|
|
436
438
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
437
|
-
top: z.number().default(100).describe("The maximum number of repositories to return."),
|
|
438
|
-
skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
|
|
439
|
+
top: z.coerce.number().default(100).describe("The maximum number of repositories to return."),
|
|
440
|
+
skip: z.coerce.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
|
|
439
441
|
repoNameFilter: z.string().optional().describe("Optional filter to search for repositories by name. If provided, only repositories with names containing this string will be returned."),
|
|
440
442
|
}, async ({ project, top, skip, repoNameFilter }) => {
|
|
441
443
|
try {
|
|
@@ -472,8 +474,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
472
474
|
.optional()
|
|
473
475
|
.describe("The ID or name of the repository where the pull requests are located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
474
476
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID, or to scope the search to a specific project."),
|
|
475
|
-
top: z.number().default(100).describe("The maximum number of pull requests to return."),
|
|
476
|
-
skip: z.number().default(0).describe("The number of pull requests to skip."),
|
|
477
|
+
top: z.coerce.number().default(100).describe("The maximum number of pull requests to return."),
|
|
478
|
+
skip: z.coerce.number().default(0).describe("The number of pull requests to skip."),
|
|
477
479
|
created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
|
|
478
480
|
created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
|
|
479
481
|
i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
|
|
@@ -599,12 +601,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
599
601
|
repositoryId: z
|
|
600
602
|
.string()
|
|
601
603
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
602
|
-
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve threads."),
|
|
604
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request for which to retrieve threads."),
|
|
603
605
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
604
|
-
iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
605
|
-
baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
606
|
-
top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
|
|
607
|
-
skip: z.number().default(0).describe("The number of threads to skip after filtering."),
|
|
606
|
+
iteration: z.coerce.number().min(1).optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
|
|
607
|
+
baseIteration: z.coerce.number().min(1).optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
|
|
608
|
+
top: z.coerce.number().default(100).describe("The maximum number of threads to return after filtering."),
|
|
609
|
+
skip: z.coerce.number().default(0).describe("The number of threads to skip after filtering."),
|
|
608
610
|
fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
|
|
609
611
|
status: z
|
|
610
612
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
@@ -659,11 +661,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
659
661
|
repositoryId: z
|
|
660
662
|
.string()
|
|
661
663
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
662
|
-
pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
|
|
663
|
-
threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
|
|
664
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request for which to retrieve thread comments."),
|
|
665
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread for which to retrieve comments."),
|
|
664
666
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
665
|
-
top: z.number().default(100).describe("The maximum number of comments to return."),
|
|
666
|
-
skip: z.number().default(0).describe("The number of comments to skip."),
|
|
667
|
+
top: z.coerce.number().default(100).describe("The maximum number of comments to return."),
|
|
668
|
+
skip: z.coerce.number().default(0).describe("The number of comments to skip."),
|
|
667
669
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
|
|
668
670
|
}, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
|
|
669
671
|
try {
|
|
@@ -695,7 +697,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
695
697
|
repositoryId: z
|
|
696
698
|
.string()
|
|
697
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."),
|
|
698
|
-
top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
700
|
+
top: z.coerce.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
|
|
699
701
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
700
702
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
701
703
|
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
@@ -720,7 +722,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
720
722
|
repositoryId: z
|
|
721
723
|
.string()
|
|
722
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."),
|
|
723
|
-
top: z.number().default(100).describe("The maximum number of branches to return."),
|
|
725
|
+
top: z.coerce.number().default(100).describe("The maximum number of branches to return."),
|
|
724
726
|
filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
|
|
725
727
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
726
728
|
}, async ({ repositoryId, top, filterContains, project }) => {
|
|
@@ -805,7 +807,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
805
807
|
repositoryId: z
|
|
806
808
|
.string()
|
|
807
809
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
808
|
-
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
810
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to retrieve."),
|
|
809
811
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
810
812
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
811
813
|
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
@@ -855,12 +857,328 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
855
857
|
};
|
|
856
858
|
}
|
|
857
859
|
});
|
|
860
|
+
server.tool(REPO_TOOLS.get_pull_request_changes, "Get the file changes (diff) for a pull request iteration with actual code diff content. Returns the code changes including line-by-line diffs made in the pull request.", {
|
|
861
|
+
repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
|
|
862
|
+
pullRequestId: z.number().describe("The ID of the pull request to retrieve changes for."),
|
|
863
|
+
iterationId: z.number().optional().describe("The iteration ID to get changes for. If not specified, gets changes for the latest iteration."),
|
|
864
|
+
project: z.string().optional().describe("Project ID or project name (optional)"),
|
|
865
|
+
top: z.number().optional().describe("Maximum number of files to include diffs for. Default is 100."),
|
|
866
|
+
skip: z.number().optional().describe("Number of changes to skip for pagination."),
|
|
867
|
+
compareTo: z.number().optional().describe("Iteration ID to compare against. If specified, returns changes between two iterations."),
|
|
868
|
+
includeDiffs: z.boolean().optional().describe("Whether to include actual line-by-line diff content. Default is true. Set to false to get only file metadata."),
|
|
869
|
+
includeLineContent: z
|
|
870
|
+
.boolean()
|
|
871
|
+
.optional()
|
|
872
|
+
.describe("Whether to include the actual line content from the changed files. Default is true. When true, fetches file content and includes the actual code lines that were added/removed/modified."),
|
|
873
|
+
}, async ({ repositoryId, pullRequestId, iterationId, project, top, skip, compareTo, includeDiffs = true, includeLineContent = true }) => {
|
|
874
|
+
try {
|
|
875
|
+
const connection = await connectionProvider();
|
|
876
|
+
const gitApi = await connection.getGitApi();
|
|
877
|
+
// If repositoryId is a name (not a GUID), we need a project to resolve it.
|
|
878
|
+
// GUID pattern: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
879
|
+
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);
|
|
880
|
+
if (!isGuid && !project) {
|
|
881
|
+
return {
|
|
882
|
+
content: [
|
|
883
|
+
{
|
|
884
|
+
type: "text",
|
|
885
|
+
text: "Error: When using a repository name instead of a GUID for repositoryId, the 'project' parameter is required. Please either provide the project name/ID, or use repo_get_repo_by_name_or_id to resolve the repository GUID first.",
|
|
886
|
+
},
|
|
887
|
+
],
|
|
888
|
+
isError: true,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
// If no iteration ID provided, get the latest iteration
|
|
892
|
+
let targetIterationId = iterationId;
|
|
893
|
+
let targetIteration;
|
|
894
|
+
if (targetIterationId == null) {
|
|
895
|
+
const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project);
|
|
896
|
+
if (!iterations || iterations.length === 0) {
|
|
897
|
+
return {
|
|
898
|
+
content: [{ type: "text", text: "No iterations found for this pull request." }],
|
|
899
|
+
isError: true,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
// Get the latest iteration
|
|
903
|
+
targetIteration = iterations[iterations.length - 1];
|
|
904
|
+
targetIterationId = targetIteration.id;
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
// Get the specific iteration
|
|
908
|
+
targetIteration = await gitApi.getPullRequestIteration(repositoryId, pullRequestId, targetIterationId, project);
|
|
909
|
+
}
|
|
910
|
+
// Get the file change metadata
|
|
911
|
+
const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, targetIterationId ?? 1, project, top, skip, compareTo);
|
|
912
|
+
// If includeDiffs is false, just return the metadata
|
|
913
|
+
if (!includeDiffs) {
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
// Get actual diff content using getFileDiffs
|
|
919
|
+
if (changes.changeEntries && changes.changeEntries.length > 0 && targetIteration) {
|
|
920
|
+
// Determine base and target commits
|
|
921
|
+
const baseCommitId = compareTo
|
|
922
|
+
? (await gitApi.getPullRequestIteration(repositoryId, pullRequestId, compareTo, project)).sourceRefCommit?.commitId
|
|
923
|
+
: targetIteration.commonRefCommit?.commitId;
|
|
924
|
+
const targetCommitId = targetIteration.sourceRefCommit?.commitId;
|
|
925
|
+
if (baseCommitId && targetCommitId) {
|
|
926
|
+
// Build FileDiffsCriteria with paths from changeEntries
|
|
927
|
+
// Exclude added and deleted files as they don't have both versions to diff
|
|
928
|
+
// changeType is a flags enum so use bitwise AND to check
|
|
929
|
+
const fileDiffParams = changes.changeEntries
|
|
930
|
+
.filter((entry) => {
|
|
931
|
+
const ct = entry.changeType ?? 0;
|
|
932
|
+
return entry.item?.path && !(ct & VersionControlChangeType.Add) && !(ct & VersionControlChangeType.Delete);
|
|
933
|
+
})
|
|
934
|
+
.map((entry) => {
|
|
935
|
+
// Remove leading slash if present - Azure DevOps API expects relative paths
|
|
936
|
+
const itemPath = entry.item?.path ?? "";
|
|
937
|
+
const path = itemPath.startsWith("/") ? itemPath.substring(1) : itemPath;
|
|
938
|
+
// For renamed/moved files, use the original path from the change entry
|
|
939
|
+
const origPath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : path;
|
|
940
|
+
return {
|
|
941
|
+
path: path,
|
|
942
|
+
originalPath: origPath,
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
if (fileDiffParams.length > 0) {
|
|
946
|
+
try {
|
|
947
|
+
// Azure DevOps getFileDiffs API accepts max 10 files per request
|
|
948
|
+
const FILE_DIFF_BATCH_SIZE = 10;
|
|
949
|
+
let fileDiffs = [];
|
|
950
|
+
for (let i = 0; i < fileDiffParams.length; i += FILE_DIFF_BATCH_SIZE) {
|
|
951
|
+
const batch = fileDiffParams.slice(i, i + FILE_DIFF_BATCH_SIZE);
|
|
952
|
+
const batchDiffs = await gitApi.getFileDiffs({
|
|
953
|
+
baseVersionCommit: baseCommitId,
|
|
954
|
+
targetVersionCommit: targetCommitId,
|
|
955
|
+
fileDiffParams: batch,
|
|
956
|
+
}, project || "", repositoryId);
|
|
957
|
+
fileDiffs = fileDiffs.concat(batchDiffs);
|
|
958
|
+
}
|
|
959
|
+
// Merge diff content with change metadata
|
|
960
|
+
const enrichedChanges = {
|
|
961
|
+
...changes,
|
|
962
|
+
changeEntries: changes.changeEntries.map((entry) => {
|
|
963
|
+
// Normalize path for comparison (remove leading slash)
|
|
964
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
965
|
+
const matchingDiff = fileDiffs.find((diff) => diff.path === entryPath);
|
|
966
|
+
return {
|
|
967
|
+
...entry,
|
|
968
|
+
diff: matchingDiff || null,
|
|
969
|
+
};
|
|
970
|
+
}),
|
|
971
|
+
};
|
|
972
|
+
// If includeLineContent is true, fetch actual file content with concurrency limit
|
|
973
|
+
if (includeLineContent && enrichedChanges.changeEntries) {
|
|
974
|
+
const CONCURRENCY_LIMIT = 10;
|
|
975
|
+
const entriesWithContent = [...enrichedChanges.changeEntries];
|
|
976
|
+
for (let i = 0; i < entriesWithContent.length; i += CONCURRENCY_LIMIT) {
|
|
977
|
+
const batch = entriesWithContent.slice(i, i + CONCURRENCY_LIMIT);
|
|
978
|
+
const batchResults = await Promise.all(batch.map(async (entry) => {
|
|
979
|
+
const ct = entry.changeType ?? 0;
|
|
980
|
+
const isAdd = !!(ct & VersionControlChangeType.Add);
|
|
981
|
+
const isDelete = !!(ct & VersionControlChangeType.Delete);
|
|
982
|
+
const entryPath = entry.item?.path?.startsWith("/") ? entry.item.path.substring(1) : entry.item?.path;
|
|
983
|
+
if (!entryPath) {
|
|
984
|
+
return entry;
|
|
985
|
+
}
|
|
986
|
+
// Handle added files: fetch full content at target commit and create synthetic diff
|
|
987
|
+
if (isAdd && !entry.diff) {
|
|
988
|
+
try {
|
|
989
|
+
const targetStream = await gitApi
|
|
990
|
+
.getItemText(repositoryId, entryPath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
991
|
+
.catch(() => null);
|
|
992
|
+
if (targetStream) {
|
|
993
|
+
const targetText = await streamToString(targetStream);
|
|
994
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
995
|
+
return {
|
|
996
|
+
...entry,
|
|
997
|
+
diff: {
|
|
998
|
+
path: entryPath,
|
|
999
|
+
originalPath: entryPath,
|
|
1000
|
+
lineDiffBlocks: [
|
|
1001
|
+
{
|
|
1002
|
+
changeType: 1, // Add
|
|
1003
|
+
originalLineNumberStart: 0,
|
|
1004
|
+
originalLinesCount: 0,
|
|
1005
|
+
modifiedLineNumberStart: 1,
|
|
1006
|
+
modifiedLinesCount: targetLines.length,
|
|
1007
|
+
modifiedLines: targetLines,
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch (addError) {
|
|
1015
|
+
return {
|
|
1016
|
+
...entry,
|
|
1017
|
+
_contentFetchError: `Failed to fetch added file content: ${addError instanceof Error ? addError.message : "Unknown error"}`,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return entry;
|
|
1021
|
+
}
|
|
1022
|
+
// Handle deleted files: fetch full content at base commit and create synthetic diff
|
|
1023
|
+
if (isDelete && !entry.diff) {
|
|
1024
|
+
try {
|
|
1025
|
+
const basePath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : entryPath;
|
|
1026
|
+
const baseStream = await gitApi
|
|
1027
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1028
|
+
.catch(() => null);
|
|
1029
|
+
if (baseStream) {
|
|
1030
|
+
const baseText = await streamToString(baseStream);
|
|
1031
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1032
|
+
return {
|
|
1033
|
+
...entry,
|
|
1034
|
+
diff: {
|
|
1035
|
+
path: entryPath,
|
|
1036
|
+
originalPath: basePath,
|
|
1037
|
+
lineDiffBlocks: [
|
|
1038
|
+
{
|
|
1039
|
+
changeType: 2, // Delete
|
|
1040
|
+
originalLineNumberStart: 1,
|
|
1041
|
+
originalLinesCount: baseLines.length,
|
|
1042
|
+
modifiedLineNumberStart: 0,
|
|
1043
|
+
modifiedLinesCount: 0,
|
|
1044
|
+
originalLines: baseLines,
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
catch (delError) {
|
|
1052
|
+
return {
|
|
1053
|
+
...entry,
|
|
1054
|
+
_contentFetchError: `Failed to fetch deleted file content: ${delError instanceof Error ? delError.message : "Unknown error"}`,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return entry;
|
|
1058
|
+
}
|
|
1059
|
+
// For modified/renamed files, skip if no diff blocks
|
|
1060
|
+
if (!entry.diff?.lineDiffBlocks || entry.diff.lineDiffBlocks.length === 0) {
|
|
1061
|
+
return entry;
|
|
1062
|
+
}
|
|
1063
|
+
// For renamed/moved files, the base version is at the original path
|
|
1064
|
+
const basePath = entry.originalPath ? (entry.originalPath.startsWith("/") ? entry.originalPath.substring(1) : entry.originalPath) : entryPath;
|
|
1065
|
+
try {
|
|
1066
|
+
// Fetch file content at both commits
|
|
1067
|
+
const [baseContent, targetContent] = await Promise.all([
|
|
1068
|
+
// Base version (original) - use basePath for renamed files
|
|
1069
|
+
gitApi
|
|
1070
|
+
.getItemText(repositoryId, basePath, project, undefined, undefined, undefined, undefined, undefined, { version: baseCommitId, versionType: GitVersionType.Commit })
|
|
1071
|
+
.catch(() => null),
|
|
1072
|
+
// Target version (modified)
|
|
1073
|
+
gitApi
|
|
1074
|
+
.getItemText(repositoryId, entryPath, project, undefined, undefined, undefined, undefined, undefined, { version: targetCommitId, versionType: GitVersionType.Commit })
|
|
1075
|
+
.catch(() => null),
|
|
1076
|
+
]);
|
|
1077
|
+
// Convert streams to text
|
|
1078
|
+
const baseText = baseContent ? await streamToString(baseContent) : "";
|
|
1079
|
+
const targetText = targetContent ? await streamToString(targetContent) : "";
|
|
1080
|
+
// Check if response is an Azure DevOps error (returned as JSON in the stream)
|
|
1081
|
+
const checkForApiError = (text, label) => {
|
|
1082
|
+
if (text.startsWith("{")) {
|
|
1083
|
+
try {
|
|
1084
|
+
const parsed = JSON.parse(text);
|
|
1085
|
+
if (parsed.$id && parsed.innerException !== undefined) {
|
|
1086
|
+
throw new Error(`Failed to fetch ${label} file content: ${parsed.message || text}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
catch (e) {
|
|
1090
|
+
if (e instanceof Error && e.message.startsWith("Failed to fetch"))
|
|
1091
|
+
throw e;
|
|
1092
|
+
// Not valid JSON or not an error response — treat as legitimate content
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
checkForApiError(baseText, "base");
|
|
1097
|
+
checkForApiError(targetText, "target");
|
|
1098
|
+
// Split into lines
|
|
1099
|
+
const baseLines = baseText.split(/\r?\n/);
|
|
1100
|
+
const targetLines = targetText.split(/\r?\n/);
|
|
1101
|
+
// Enrich each lineDiffBlock with actual line content
|
|
1102
|
+
const enrichedDiff = {
|
|
1103
|
+
...entry.diff,
|
|
1104
|
+
lineDiffBlocks: entry.diff.lineDiffBlocks?.map((block) => {
|
|
1105
|
+
const enrichedBlock = { ...block };
|
|
1106
|
+
// Add original (base) lines if they exist
|
|
1107
|
+
if (block.originalLineNumberStart && block.originalLinesCount) {
|
|
1108
|
+
const startIdx = block.originalLineNumberStart - 1;
|
|
1109
|
+
const endIdx = startIdx + block.originalLinesCount;
|
|
1110
|
+
enrichedBlock.originalLines = baseLines.slice(startIdx, endIdx);
|
|
1111
|
+
}
|
|
1112
|
+
// Add modified (target) lines if they exist
|
|
1113
|
+
if (block.modifiedLineNumberStart && block.modifiedLinesCount) {
|
|
1114
|
+
const startIdx = block.modifiedLineNumberStart - 1;
|
|
1115
|
+
const endIdx = startIdx + block.modifiedLinesCount;
|
|
1116
|
+
enrichedBlock.modifiedLines = targetLines.slice(startIdx, endIdx);
|
|
1117
|
+
}
|
|
1118
|
+
return enrichedBlock;
|
|
1119
|
+
}),
|
|
1120
|
+
};
|
|
1121
|
+
return {
|
|
1122
|
+
...entry,
|
|
1123
|
+
diff: enrichedDiff,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
catch (contentError) {
|
|
1127
|
+
// If content fetch fails, return entry with error
|
|
1128
|
+
return {
|
|
1129
|
+
...entry,
|
|
1130
|
+
_contentFetchError: `Failed to fetch line content: ${contentError instanceof Error ? contentError.message : "Unknown error"}`,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
}));
|
|
1134
|
+
// Write batch results back into the array
|
|
1135
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
1136
|
+
entriesWithContent[i + j] = batchResults[j];
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
enrichedChanges.changeEntries = entriesWithContent;
|
|
1140
|
+
}
|
|
1141
|
+
return {
|
|
1142
|
+
content: [{ type: "text", text: JSON.stringify(enrichedChanges, null, 2) }],
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
catch (diffError) {
|
|
1146
|
+
// If diff fetching fails, return metadata with error info
|
|
1147
|
+
return {
|
|
1148
|
+
content: [
|
|
1149
|
+
{
|
|
1150
|
+
type: "text",
|
|
1151
|
+
text: JSON.stringify({
|
|
1152
|
+
...changes,
|
|
1153
|
+
_diffError: `Failed to fetch diff content: ${diffError instanceof Error ? diffError.message : "Unknown error"}`,
|
|
1154
|
+
_note: "Returned metadata only",
|
|
1155
|
+
}, null, 2),
|
|
1156
|
+
},
|
|
1157
|
+
],
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Fallback: return metadata if we couldn't get diffs
|
|
1164
|
+
return {
|
|
1165
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1170
|
+
return {
|
|
1171
|
+
content: [{ type: "text", text: `Error getting pull request changes: ${errorMessage}` }],
|
|
1172
|
+
isError: true,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
858
1176
|
server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
|
|
859
1177
|
repositoryId: z
|
|
860
1178
|
.string()
|
|
861
1179
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
862
|
-
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
863
|
-
threadId: z.number().describe("The ID of the thread to which the comment will be added."),
|
|
1180
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
1181
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread to which the comment will be added."),
|
|
864
1182
|
content: z.string().describe("The content of the comment to be added."),
|
|
865
1183
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
866
1184
|
fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
|
|
@@ -897,7 +1215,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
897
1215
|
repositoryId: z
|
|
898
1216
|
.string()
|
|
899
1217
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
900
|
-
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
1218
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
901
1219
|
content: z.string().describe("The content of the comment to be added."),
|
|
902
1220
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
903
1221
|
filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
|
|
@@ -906,7 +1224,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
906
1224
|
.optional()
|
|
907
1225
|
.default(CommentThreadStatus[CommentThreadStatus.Active])
|
|
908
1226
|
.describe("The status of the comment thread. Defaults to 'Active'."),
|
|
909
|
-
rightFileStartLine: z.
|
|
1227
|
+
rightFileStartLine: z.coerce
|
|
1228
|
+
.number()
|
|
1229
|
+
.min(1)
|
|
1230
|
+
.optional()
|
|
1231
|
+
.describe("Position of first character of the thread's span in right file. The line number of a thread's position. Starts at 1. (optional)"),
|
|
910
1232
|
rightFileStartOffset: z
|
|
911
1233
|
.number()
|
|
912
1234
|
.optional()
|
|
@@ -1013,8 +1335,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1013
1335
|
repositoryId: z
|
|
1014
1336
|
.string()
|
|
1015
1337
|
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
1016
|
-
pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
|
|
1017
|
-
threadId: z.number().describe("The ID of the thread to update."),
|
|
1338
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request where the comment thread exists."),
|
|
1339
|
+
threadId: z.coerce.number().min(1).describe("The ID of the thread to update."),
|
|
1018
1340
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
1019
1341
|
status: z
|
|
1020
1342
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
@@ -1067,8 +1389,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1067
1389
|
.optional()
|
|
1068
1390
|
.default(GitVersionType[GitVersionType.Branch])
|
|
1069
1391
|
.describe("The meaning of the version parameter, e.g., branch, tag or commit"),
|
|
1070
|
-
skip: z.number().optional().default(0).describe("Number of commits to skip"),
|
|
1071
|
-
top: z.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
1392
|
+
skip: z.coerce.number().optional().default(0).describe("Number of commits to skip"),
|
|
1393
|
+
top: z.coerce.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
1072
1394
|
includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
|
|
1073
1395
|
includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
|
|
1074
1396
|
// Enhanced search parameters
|
|
@@ -1224,7 +1546,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1224
1546
|
});
|
|
1225
1547
|
server.tool(REPO_TOOLS.vote_pull_request, "Cast a vote on a pull request. Automatically adds the current user as a reviewer if they are not already one.", {
|
|
1226
1548
|
repositoryId: z.string().describe("The ID or name of the repository. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
1227
|
-
pullRequestId: z.number().describe("The ID of the pull request."),
|
|
1549
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
|
|
1228
1550
|
vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
|
|
1229
1551
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
1230
1552
|
}, async ({ repositoryId, pullRequestId, vote, project }) => {
|
|
@@ -1259,7 +1581,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1259
1581
|
version: z.string().optional().describe("The version identifier - branch name (e.g., 'main'), tag name, or commit SHA. Defaults to the repository's default branch."),
|
|
1260
1582
|
versionType: z.enum(["Branch", "Commit", "Tag"]).optional().default("Branch").describe("The type of version identifier: 'Branch', 'Commit', or 'Tag'. Defaults to 'Branch'."),
|
|
1261
1583
|
recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Defaults to false."),
|
|
1262
|
-
recursionDepth: z.number().optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1584
|
+
recursionDepth: z.coerce.number().min(1).optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1263
1585
|
}, async ({ repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
|
|
1264
1586
|
try {
|
|
1265
1587
|
const connection = await connectionProvider();
|
|
@@ -1318,5 +1640,58 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1318
1640
|
};
|
|
1319
1641
|
}
|
|
1320
1642
|
});
|
|
1643
|
+
// ── Get file content at a specific version (branch, tag, or commit) ──
|
|
1644
|
+
const fileVersionTypeStrings = getEnumKeys(GitVersionType);
|
|
1645
|
+
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). " +
|
|
1646
|
+
"Useful for reading source files from PR branches, specific commits, or tags without having them checked out locally.", {
|
|
1647
|
+
repositoryId: z.string().describe("The ID (GUID) or name of the repository."),
|
|
1648
|
+
path: z.string().describe("The full path to the file in the repository, e.g., '/src/main.ts' or 'src/main.ts'."),
|
|
1649
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name."),
|
|
1650
|
+
version: z
|
|
1651
|
+
.string()
|
|
1652
|
+
.optional()
|
|
1653
|
+
.describe("Version string: branch name (e.g. 'main'), tag name, or commit SHA. " + "Defaults to the repository's default branch if not specified."),
|
|
1654
|
+
versionType: z
|
|
1655
|
+
.enum(fileVersionTypeStrings)
|
|
1656
|
+
.optional()
|
|
1657
|
+
.default("Commit")
|
|
1658
|
+
.describe("How to interpret the 'version' parameter. Defaults to 'Commit'."),
|
|
1659
|
+
}, async ({ repositoryId, path, project, version, versionType }) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const connection = await connectionProvider();
|
|
1662
|
+
const gitApi = await connection.getGitApi();
|
|
1663
|
+
// Build the version descriptor if a version was specified
|
|
1664
|
+
const versionDescriptor = version
|
|
1665
|
+
? {
|
|
1666
|
+
version: version,
|
|
1667
|
+
versionType: GitVersionType[versionType],
|
|
1668
|
+
}
|
|
1669
|
+
: undefined;
|
|
1670
|
+
// getItemText returns a ReadableStream of the file content as text
|
|
1671
|
+
const stream = await gitApi.getItemText(repositoryId, path, project, undefined, // scopePath
|
|
1672
|
+
undefined, // recursionLevel
|
|
1673
|
+
undefined, // includeContentMetadata
|
|
1674
|
+
undefined, // latestProcessedChange
|
|
1675
|
+
false, // download
|
|
1676
|
+
versionDescriptor, true // includeContent
|
|
1677
|
+
);
|
|
1678
|
+
const content = await streamToString(stream);
|
|
1679
|
+
return {
|
|
1680
|
+
content: [{ type: "text", text: content }],
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
catch (error) {
|
|
1684
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1685
|
+
return {
|
|
1686
|
+
content: [
|
|
1687
|
+
{
|
|
1688
|
+
type: "text",
|
|
1689
|
+
text: `Error getting file content for '${path}': ${errorMessage}`,
|
|
1690
|
+
},
|
|
1691
|
+
],
|
|
1692
|
+
isError: true,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1321
1696
|
}
|
|
1322
1697
|
export { REPO_TOOLS, configureRepoTools };
|
package/dist/tools/search.js
CHANGED
|
@@ -17,8 +17,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
17
17
|
path: z.array(z.string()).optional().describe("Filter by paths"),
|
|
18
18
|
branch: z.array(z.string()).optional().describe("Filter by branches"),
|
|
19
19
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
20
|
-
skip: z.number().default(0).describe("Number of results to skip"),
|
|
21
|
-
top: z.number().default(5).describe("Maximum number of results to return"),
|
|
20
|
+
skip: z.coerce.number().default(0).describe("Number of results to skip"),
|
|
21
|
+
top: z.coerce.number().default(5).describe("Maximum number of results to return"),
|
|
22
22
|
}, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
|
|
23
23
|
const accessToken = await tokenProvider();
|
|
24
24
|
const connection = await connectionProvider();
|
|
@@ -66,8 +66,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
66
66
|
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
67
67
|
wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
|
|
68
68
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
69
|
-
skip: z.number().default(0).describe("Number of results to skip"),
|
|
70
|
-
top: z.number().default(10).describe("Maximum number of results to return"),
|
|
69
|
+
skip: z.coerce.number().default(0).describe("Number of results to skip"),
|
|
70
|
+
top: z.coerce.number().default(10).describe("Maximum number of results to return"),
|
|
71
71
|
}, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
|
|
72
72
|
const accessToken = await tokenProvider();
|
|
73
73
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
@@ -110,8 +110,8 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
|
|
|
110
110
|
state: z.array(z.string()).optional().describe("Filter by work item states"),
|
|
111
111
|
assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
|
|
112
112
|
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
113
|
-
skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
114
|
-
top: z.number().default(10).describe("Number of results to return"),
|
|
113
|
+
skip: z.coerce.number().default(0).describe("Number of results to skip for pagination"),
|
|
114
|
+
top: z.coerce.number().default(10).describe("Number of results to return"),
|
|
115
115
|
}, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
|
|
116
116
|
const accessToken = await tokenProvider();
|
|
117
117
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -72,8 +72,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
72
72
|
});
|
|
73
73
|
server.tool(Test_Plan_Tools.create_test_suite, "Creates a new test suite in a test plan.", {
|
|
74
74
|
project: z.string().describe("Project ID or project name"),
|
|
75
|
-
planId: z.number().describe("ID of the test plan that contains the suites"),
|
|
76
|
-
parentSuiteId: z.number().describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
|
|
75
|
+
planId: z.coerce.number().min(1).describe("ID of the test plan that contains the suites"),
|
|
76
|
+
parentSuiteId: z.coerce.number().min(1).describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
|
|
77
77
|
name: z.string().describe("Name of the child test suite"),
|
|
78
78
|
}, async ({ project, planId, parentSuiteId, name }) => {
|
|
79
79
|
const maxRetries = 5;
|
|
@@ -120,8 +120,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
120
120
|
});
|
|
121
121
|
server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
|
|
122
122
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
123
|
-
planId: z.number().describe("The ID of the test plan."),
|
|
124
|
-
suiteId: z.number().describe("The ID of the test suite."),
|
|
123
|
+
planId: z.coerce.number().min(1).describe("The ID of the test plan."),
|
|
124
|
+
suiteId: z.coerce.number().min(1).describe("The ID of the test suite."),
|
|
125
125
|
testCaseIds: z.string().or(z.array(z.string())).describe("The ID(s) of the test case(s) to add. "),
|
|
126
126
|
}, async ({ project, planId, suiteId, testCaseIds }) => {
|
|
127
127
|
try {
|
|
@@ -149,10 +149,10 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
149
149
|
.string()
|
|
150
150
|
.optional()
|
|
151
151
|
.describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
|
|
152
|
-
priority: z.number().optional().describe("The priority of the test case."),
|
|
152
|
+
priority: z.coerce.number().optional().describe("The priority of the test case."),
|
|
153
153
|
areaPath: z.string().optional().describe("The area path for the test case."),
|
|
154
154
|
iterationPath: z.string().optional().describe("The iteration path for the test case."),
|
|
155
|
-
testsWorkItemId: z.number().optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
|
|
155
|
+
testsWorkItemId: z.coerce.number().min(1).optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
|
|
156
156
|
}, async ({ project, title, steps, priority, areaPath, iterationPath, testsWorkItemId }) => {
|
|
157
157
|
try {
|
|
158
158
|
const connection = await connectionProvider();
|
|
@@ -220,7 +220,7 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
server.tool(Test_Plan_Tools.update_test_case_steps, "Update an existing test case work item.", {
|
|
223
|
-
id: z.number().describe("The ID of the test case work item to update."),
|
|
223
|
+
id: z.coerce.number().min(1).describe("The ID of the test case work item to update."),
|
|
224
224
|
steps: z
|
|
225
225
|
.string()
|
|
226
226
|
.describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
|
|
@@ -256,8 +256,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
256
256
|
});
|
|
257
257
|
server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
|
|
258
258
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
259
|
-
planid: z.number().describe("The ID of the test plan."),
|
|
260
|
-
suiteid: z.number().describe("The ID of the test suite."),
|
|
259
|
+
planid: z.coerce.number().min(1).describe("The ID of the test plan."),
|
|
260
|
+
suiteid: z.coerce.number().min(1).describe("The ID of the test suite."),
|
|
261
261
|
}, async ({ project, planid, suiteid }) => {
|
|
262
262
|
try {
|
|
263
263
|
const connection = await connectionProvider();
|
|
@@ -277,7 +277,7 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
277
277
|
});
|
|
278
278
|
server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID. Can filter by test outcome (e.g. Failed, Passed, Aborted). Returns test case titles, error messages, stack traces, and outcomes. Efficiently handles builds with large numbers of test runs.", {
|
|
279
279
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
280
|
-
buildid: z.number().describe("The ID of the build."),
|
|
280
|
+
buildid: z.coerce.number().min(1).describe("The ID of the build."),
|
|
281
281
|
outcomes: z.array(z.string()).optional().describe("Filter results by test outcome, e.g. ['Failed', 'Passed', 'Aborted']."),
|
|
282
282
|
}, async ({ project, buildid, outcomes }) => {
|
|
283
283
|
try {
|
|
@@ -329,7 +329,7 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
329
329
|
});
|
|
330
330
|
server.tool(Test_Plan_Tools.list_test_suites, "Retrieve a paginated list of test suites from an Azure DevOps project and Test Plan Id.", {
|
|
331
331
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
332
|
-
planId: z.number().describe("The ID of the test plan."),
|
|
332
|
+
planId: z.coerce.number().min(1).describe("The ID of the test plan."),
|
|
333
333
|
continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
|
|
334
334
|
}, async ({ project, planId, continuationToken }) => {
|
|
335
335
|
try {
|
package/dist/tools/wiki.js
CHANGED
|
@@ -59,9 +59,9 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
59
59
|
server.tool(WIKI_TOOLS.list_wiki_pages, "Retrieve a list of wiki pages for a specific wiki and project.", {
|
|
60
60
|
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
61
61
|
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
62
|
-
top: z.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
|
|
62
|
+
top: z.coerce.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
|
|
63
63
|
continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages."),
|
|
64
|
-
pageViewsForDays: z.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
|
|
64
|
+
pageViewsForDays: z.coerce.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
|
|
65
65
|
}, async ({ wikiIdentifier, project, top = 20, continuationToken, pageViewsForDays }) => {
|
|
66
66
|
try {
|
|
67
67
|
const connection = await connectionProvider();
|
|
@@ -111,7 +111,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
111
111
|
if (recursionLevel) {
|
|
112
112
|
params.append("recursionLevel", recursionLevel);
|
|
113
113
|
}
|
|
114
|
-
const url = `${baseUrl}/${project}/_apis/wiki/wikis/${wikiIdentifier}/pages?${params.toString()}`;
|
|
114
|
+
const url = `${baseUrl}/${encodeURIComponent(project)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?${params.toString()}`;
|
|
115
115
|
const response = await fetch(url, {
|
|
116
116
|
headers: {
|
|
117
117
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -173,7 +173,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
173
173
|
try {
|
|
174
174
|
const accessToken = await tokenProvider();
|
|
175
175
|
const baseUrl = connection.serverUrl.replace(/\/$/, "");
|
|
176
|
-
const restUrl = `${baseUrl}/${resolvedProject}/_apis/wiki/wikis/${resolvedWiki}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
|
|
176
|
+
const restUrl = `${baseUrl}/${encodeURIComponent(resolvedProject)}/_apis/wiki/wikis/${encodeURIComponent(resolvedWiki)}/pages/${parsed.pageId}?includeContent=true&api-version=7.1`;
|
|
177
177
|
const resp = await fetch(restUrl, {
|
|
178
178
|
headers: {
|
|
179
179
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -236,7 +236,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
236
236
|
// Build the URL for the wiki page API with version descriptor
|
|
237
237
|
const baseUrl = connection.serverUrl;
|
|
238
238
|
const projectParam = project || "";
|
|
239
|
-
const url = `${baseUrl}/${projectParam}/_apis/wiki/wikis/${wikiIdentifier}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
|
|
239
|
+
const url = `${baseUrl}/${encodeURIComponent(projectParam)}/_apis/wiki/wikis/${encodeURIComponent(wikiIdentifier)}/pages?path=${encodedPath}&versionDescriptor.versionType=branch&versionDescriptor.version=${encodeURIComponent(branch)}&api-version=7.1`;
|
|
240
240
|
// First, try to create a new page (PUT without ETag)
|
|
241
241
|
try {
|
|
242
242
|
const createResponse = await fetch(url, {
|
package/dist/tools/work-items.js
CHANGED
|
@@ -104,7 +104,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
104
104
|
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user.", {
|
|
105
105
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
106
106
|
type: z.enum(["assignedtome", "myactivity"]).default("assignedtome").describe("The type of work items to retrieve. Defaults to 'assignedtome'."),
|
|
107
|
-
top: z.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
107
|
+
top: z.coerce.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
108
108
|
includeCompleted: z.boolean().default(false).describe("Whether to include completed work items. Defaults to false."),
|
|
109
109
|
}, async ({ project, type, top, includeCompleted }) => {
|
|
110
110
|
try {
|
|
@@ -125,7 +125,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
125
125
|
});
|
|
126
126
|
server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
|
|
127
127
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
128
|
-
ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
|
|
128
|
+
ids: z.array(z.coerce.number().min(1)).describe("The IDs of the work items to retrieve."),
|
|
129
129
|
fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, a hardcoded default set of fields will be used."),
|
|
130
130
|
}, async ({ project, ids, fields }) => {
|
|
131
131
|
try {
|
|
@@ -174,7 +174,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID.", {
|
|
177
|
-
id: z.number().describe("The ID of the work item to retrieve."),
|
|
177
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to retrieve."),
|
|
178
178
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
179
179
|
fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, all fields will be returned."),
|
|
180
180
|
asOf: z.coerce.date().optional().describe("Optional date string to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
|
|
@@ -202,8 +202,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
202
202
|
});
|
|
203
203
|
server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
|
|
204
204
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
205
|
-
workItemId: z.number().describe("The ID of the work item to retrieve comments for."),
|
|
206
|
-
top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
|
|
205
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve comments for."),
|
|
206
|
+
top: z.coerce.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
|
|
207
207
|
}, async ({ project, workItemId, top }) => {
|
|
208
208
|
try {
|
|
209
209
|
const connection = await connectionProvider();
|
|
@@ -223,7 +223,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
223
223
|
});
|
|
224
224
|
server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
|
|
225
225
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
226
|
-
workItemId: z.number().describe("The ID of the work item to add a comment to."),
|
|
226
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to add a comment to."),
|
|
227
227
|
comment: z.string().describe("The text of the comment to add to the work item."),
|
|
228
228
|
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
229
229
|
}, async ({ project, workItemId, comment, format }) => {
|
|
@@ -235,7 +235,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
235
235
|
text: comment,
|
|
236
236
|
};
|
|
237
237
|
const formatParameter = format === "markdown" ? 0 : 1;
|
|
238
|
-
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
238
|
+
const response = await fetch(`${orgUrl}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
239
239
|
method: "POST",
|
|
240
240
|
headers: {
|
|
241
241
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -262,8 +262,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
262
262
|
});
|
|
263
263
|
server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID.", {
|
|
264
264
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
265
|
-
workItemId: z.number().describe("The ID of the work item."),
|
|
266
|
-
commentId: z.number().describe("The ID of the comment to update."),
|
|
265
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item."),
|
|
266
|
+
commentId: z.coerce.number().min(1).describe("The ID of the comment to update."),
|
|
267
267
|
text: z.string().describe("The updated comment text."),
|
|
268
268
|
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
269
269
|
}, async ({ project, workItemId, commentId, text, format }) => {
|
|
@@ -273,7 +273,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
273
273
|
const accessToken = await tokenProvider();
|
|
274
274
|
const body = { text };
|
|
275
275
|
const formatParameter = format === "markdown" ? 0 : 1;
|
|
276
|
-
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
276
|
+
const response = await fetch(`${orgUrl}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
277
277
|
method: "PATCH",
|
|
278
278
|
headers: {
|
|
279
279
|
"Authorization": `Bearer ${accessToken}`,
|
|
@@ -300,9 +300,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
300
300
|
});
|
|
301
301
|
server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID.", {
|
|
302
302
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
303
|
-
workItemId: z.number().describe("The ID of the work item to retrieve revisions for."),
|
|
304
|
-
top: z.number().default(50).describe("Optional number of revisions to retrieve. If not provided, all revisions will be returned."),
|
|
305
|
-
skip: z.number().optional().describe("Optional number of revisions to skip for pagination. Defaults to 0."),
|
|
303
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve revisions for."),
|
|
304
|
+
top: z.coerce.number().default(50).describe("Optional number of revisions to retrieve. If not provided, all revisions will be returned."),
|
|
305
|
+
skip: z.coerce.number().optional().describe("Optional number of revisions to skip for pagination. Defaults to 0."),
|
|
306
306
|
expand: z
|
|
307
307
|
.enum(getEnumKeys(WorkItemExpand))
|
|
308
308
|
.default("None")
|
|
@@ -351,7 +351,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
351
351
|
}
|
|
352
352
|
});
|
|
353
353
|
server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id.", {
|
|
354
|
-
parentId: z.number().describe("The ID of the parent work item to create a child work item under."),
|
|
354
|
+
parentId: z.coerce.number().min(1).describe("The ID of the parent work item to create a child work item under."),
|
|
355
355
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
356
356
|
workItemType: z.string().describe("The type of the child work item to create."),
|
|
357
357
|
items: z.array(z.object({
|
|
@@ -432,7 +432,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
432
432
|
}
|
|
433
433
|
return {
|
|
434
434
|
method: "PATCH",
|
|
435
|
-
uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
|
|
435
|
+
uri: `/${encodeURIComponent(project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}?api-version=${batchApiVersion}`,
|
|
436
436
|
headers: {
|
|
437
437
|
"Content-Type": "application/json-patch+json",
|
|
438
438
|
},
|
|
@@ -467,8 +467,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
467
467
|
server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
|
|
468
468
|
projectId: z.string().describe("The project ID of the Azure DevOps project (note: project name is not valid)."),
|
|
469
469
|
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
|
|
470
|
-
pullRequestId: z.number().describe("The ID of the pull request to link to."),
|
|
471
|
-
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
|
|
470
|
+
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request to link to."),
|
|
471
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to link to the pull request."),
|
|
472
472
|
pullRequestProjectId: z.string().optional().describe("The project ID containing the pull request. If not provided, defaults to the work item's project ID (for same-project linking)."),
|
|
473
473
|
}, async ({ projectId, repositoryId, pullRequestId, workItemId, pullRequestProjectId }) => {
|
|
474
474
|
try {
|
|
@@ -542,7 +542,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
542
542
|
}
|
|
543
543
|
});
|
|
544
544
|
server.tool(WORKITEM_TOOLS.update_work_item, "Update a work item by ID with specified fields.", {
|
|
545
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
545
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
546
546
|
updates: z
|
|
547
547
|
.array(z.object({
|
|
548
548
|
op: z
|
|
@@ -651,7 +651,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
651
651
|
.enum(getEnumKeys(QueryExpand))
|
|
652
652
|
.optional()
|
|
653
653
|
.describe("Optional expand parameter to include additional details in the response. Defaults to 'None'."),
|
|
654
|
-
depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
654
|
+
depth: z.coerce.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
|
|
655
655
|
includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
|
|
656
656
|
useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
|
|
657
657
|
}, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
|
|
@@ -676,7 +676,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
676
676
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. If not provided, the default project will be used."),
|
|
677
677
|
team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
|
|
678
678
|
timePrecision: z.boolean().optional().describe("Whether to include time precision in the results. Defaults to false."),
|
|
679
|
-
top: z.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
679
|
+
top: z.coerce.number().default(50).describe("The maximum number of results to return. Defaults to 50."),
|
|
680
680
|
responseType: z.enum(["full", "ids"]).default("full").describe("Response type: 'full' returns complete query results (default), 'ids' returns only work item IDs for reduced payload size."),
|
|
681
681
|
}, async ({ id, project, team, timePrecision, top, responseType }) => {
|
|
682
682
|
try {
|
|
@@ -708,7 +708,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
708
708
|
updates: z
|
|
709
709
|
.array(z.object({
|
|
710
710
|
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform on the field."),
|
|
711
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
711
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
712
712
|
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
|
|
713
713
|
value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
|
|
714
714
|
format: z.enum(["Html", "Markdown"]).optional().describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
|
|
@@ -776,8 +776,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
776
776
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
777
777
|
updates: z
|
|
778
778
|
.array(z.object({
|
|
779
|
-
id: z.number().describe("The ID of the work item to update."),
|
|
780
|
-
linkToId: z.number().describe("The ID of the work item to link to."),
|
|
779
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
780
|
+
linkToId: z.coerce.number().min(1).describe("The ID of the work item to link to."),
|
|
781
781
|
type: z
|
|
782
782
|
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
783
783
|
.default("related")
|
|
@@ -839,7 +839,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
839
839
|
});
|
|
840
840
|
server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item", {
|
|
841
841
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
842
|
-
id: z.number().describe("The ID of the work item to remove the links from."),
|
|
842
|
+
id: z.coerce.number().min(1).describe("The ID of the work item to remove the links from."),
|
|
843
843
|
type: z
|
|
844
844
|
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
|
|
845
845
|
.default("related")
|
|
@@ -902,7 +902,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
902
902
|
}
|
|
903
903
|
});
|
|
904
904
|
server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically.", {
|
|
905
|
-
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
|
|
905
|
+
workItemId: z.coerce.number().min(1).describe("The ID of the work item to add the artifact link to."),
|
|
906
906
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
907
907
|
// Option 1: Provide full URI directly
|
|
908
908
|
artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
|
|
@@ -911,8 +911,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
911
911
|
repositoryId: z.string().optional().describe("The repository ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
|
|
912
912
|
branchName: z.string().optional().describe("The branch name (e.g., 'main'). Required when linkType is 'Branch'."),
|
|
913
913
|
commitId: z.string().optional().describe("The commit SHA hash. Required when linkType is 'Fixed in Commit'."),
|
|
914
|
-
pullRequestId: z.number().optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
915
|
-
buildId: z.number().optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
914
|
+
pullRequestId: z.coerce.number().min(1).optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
|
|
915
|
+
buildId: z.coerce.number().min(1).optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
|
|
916
916
|
linkType: z
|
|
917
917
|
.enum([
|
|
918
918
|
"Branch",
|
package/dist/tools/work.js
CHANGED
|
@@ -99,8 +99,8 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
99
99
|
});
|
|
100
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
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.number().default(2).describe("Depth of children to fetch."),
|
|
103
|
-
excludedIds: z.array(z.number()).optional().describe("An optional array of iteration IDs, and thier children, that should not be returned."),
|
|
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 thier children, that should not be returned."),
|
|
104
104
|
}, async ({ project, depth, excludedIds: ids }) => {
|
|
105
105
|
try {
|
|
106
106
|
const connection = await connectionProvider();
|
package/dist/utils.js
CHANGED
|
@@ -67,3 +67,18 @@ export function encodeFormattedValue(value, format) {
|
|
|
67
67
|
const result = value.replace(/</g, "<").replace(/>/g, ">");
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert a Node.js ReadableStream to a string.
|
|
72
|
+
* Shared utility for consistent stream handling across tools.
|
|
73
|
+
*/
|
|
74
|
+
export function streamToString(stream) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let data = "";
|
|
77
|
+
stream.setEncoding("utf8");
|
|
78
|
+
stream.on("data", (chunk) => {
|
|
79
|
+
data += chunk;
|
|
80
|
+
});
|
|
81
|
+
stream.on("error", reject);
|
|
82
|
+
stream.on("end", () => resolve(data));
|
|
83
|
+
});
|
|
84
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.5.0-nightly.
|
|
1
|
+
export const packageVersion = "2.5.0-nightly.20260324";
|