@azure-devops/mcp 2.7.0-nightly.20260428 → 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.
@@ -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.string().optional().describe("Repository ID to filter build definitions"),
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
- const buildDefinitions = await buildApi.getDefinitions(project, name, repositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
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 isAbsolutePath = (value) => posix.isAbsolute(value) || win32.isAbsolute(value);
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
- if (artifactName.includes("..")) {
341
- throw new Error("Invalid artifactName: path traversal is not allowed.");
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.includes("..") || isAbsolutePath(destinationPath) || hasDriveLetter(destinationPath))) {
344
- throw new Error("Invalid destinationPath: absolute paths and path traversals are not allowed.");
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();
@@ -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 branches for a given repository.", {
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 branches for a given repository Id.", {
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."),
@@ -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
- allResults.push(...group.results);
360
+ for (const result of group.results) {
361
+ allResults.push(result);
362
+ }
361
363
  }
362
364
  }
363
365
  }
@@ -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()
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.7.0-nightly.20260428";
1
+ export const packageVersion = "2.7.0-nightly.20260429";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.7.0-nightly.20260428",
3
+ "version": "2.7.0-nightly.20260429",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",