@azure-devops/mcp 2.10.0-nightly.20260922 → 2.10.0-nightly.20260924

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.
@@ -32,6 +32,7 @@ const TOOL_ANNOTATIONS = {
32
32
  repo_branch: READ_ONLY_ANNOTATIONS,
33
33
  repo_create_branch: ADDITIVE_ANNOTATIONS,
34
34
  repo_file: READ_ONLY_ANNOTATIONS,
35
+ repo_file_write: MUTATING_ANNOTATIONS,
35
36
  repo_pull_request: READ_ONLY_ANNOTATIONS,
36
37
  repo_pull_request_org: READ_ONLY_ANNOTATIONS,
37
38
  repo_pull_request_thread: READ_ONLY_ANNOTATIONS,
@@ -1,6 +1,6 @@
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, ItemContentType, 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
6
  import { extractAdoStreamError, getEnumKeys, streamToString, apiVersion } from "../utils.js";
@@ -13,6 +13,7 @@ const REPO_TOOLS = {
13
13
  repo_pull_request_thread: "repo_pull_request_thread",
14
14
  repo_branch: "repo_branch",
15
15
  repo_file: "repo_file",
16
+ repo_file_write: "repo_file_write",
16
17
  repo_search_commits: "repo_search_commits",
17
18
  repo_pull_request_write: "repo_pull_request_write",
18
19
  repo_pull_request_thread_write: "repo_pull_request_thread_write",
@@ -199,9 +200,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
199
200
  project: z.string().optional().describe("Project ID or project name. Required for list_by_commits. Optional for get and list."),
200
201
  includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to include work item references. Used for get."),
201
202
  includeLabels: z.boolean().optional().default(false).describe("Whether to include labels. Used for get."),
202
- includeChangedFiles: z.boolean().optional().default(false).describe("Whether to include the list of changed files. Used for get."),
203
- top: z.coerce.number().default(100).describe("The maximum number of pull requests to return. Used for list. Defaults to 100."),
204
- skip: z.coerce.number().default(0).describe("The number of pull requests to skip. Used for list. Defaults to 0."),
203
+ includeChangedFiles: z
204
+ .boolean()
205
+ .optional()
206
+ .default(false)
207
+ .describe("Whether to include the list of changed files. When changedFilesSummary returns a nextSkip greater than 0, pass nextSkip and nextTop back as skip and top for the next page. Used for get."),
208
+ top: z.coerce.number().default(100).describe("The maximum number of pull requests to return (list), or of changed files to return (get with includeChangedFiles). Defaults to 100."),
209
+ skip: z.coerce.number().default(0).describe("The number of pull requests to skip (list), or of changed files to skip (get with includeChangedFiles). Defaults to 0."),
205
210
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user. Used for list."),
206
211
  created_by_user: z.string().optional().describe("Filter pull requests created by a specific user email. Used for list."),
207
212
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer. Used for list."),
@@ -249,7 +254,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
249
254
  if (iterations?.length) {
250
255
  const latestIteration = iterations[iterations.length - 1];
251
256
  if (latestIteration.id != null) {
252
- const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration.id, project);
257
+ const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration.id, project, top, skip);
253
258
  enhancedResponse = {
254
259
  ...enhancedResponse,
255
260
  changedFilesSummary: {
@@ -506,8 +511,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
506
511
  versionType: z
507
512
  .enum(fileVersionTypeStrings)
508
513
  .optional()
509
- .default("Commit")
510
- .describe("How to interpret the version parameter. Used for get_content. Defaults to 'Commit'."),
514
+ .describe("How to interpret the version parameter. Defaults to 'Commit' for get_content and 'Branch' for list_directory."),
511
515
  recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Used for list_directory. Defaults to false."),
512
516
  recursionDepth: z.coerce.number().min(1).optional().default(1).describe("Maximum depth for recursive listing. Used for list_directory when recursive is true. Defaults to 1."),
513
517
  }, async ({ action, repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
@@ -517,7 +521,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
517
521
  if (action === "get_content") {
518
522
  if (!path)
519
523
  return { content: [{ type: "text", text: "path is required for get_content" }], isError: true };
520
- const versionDescriptor = version ? { version, versionType: GitVersionType[versionType] } : undefined;
524
+ const versionDescriptor = buildVersionDescriptor(version, versionType ?? "Commit");
521
525
  const stream = await gitApi.getItemText(repositoryId, path, project, undefined, undefined, undefined, undefined, false, versionDescriptor, true);
522
526
  const content = await streamToString(stream);
523
527
  const streamError = extractAdoStreamError(content);
@@ -527,7 +531,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
527
531
  return createExternalContentResponse(content, "repository file");
528
532
  }
529
533
  if (action === "list_directory") {
530
- const versionDescriptor = buildVersionDescriptor(version, versionType === "Commit" ? "Branch" : versionType);
534
+ const versionDescriptor = buildVersionDescriptor(version, versionType);
531
535
  const clampedDepth = Math.min(Math.max(recursionDepth || 1, 1), 10);
532
536
  const recursionType = recursive ? VersionControlRecursionType.Full : VersionControlRecursionType.OneLevel;
533
537
  const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
@@ -568,6 +572,63 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
568
572
  return { content: [{ type: "text", text: `Error with file operation: ${errorMessage}` }], isError: true };
569
573
  }
570
574
  });
575
+ // --- repo_file_write -------------------------------------------------------
576
+ server.tool(REPO_TOOLS.repo_file_write, "Create or update a text file in an existing repository branch and commit the change.", {
577
+ action: z.enum(["create", "update"]).describe("Whether to create a new file or update an existing file."),
578
+ repositoryId: z.string().describe("The ID or name of the repository."),
579
+ project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name."),
580
+ branchName: z.string().min(1).describe("The name of the existing branch to update, without the 'refs/heads/' prefix."),
581
+ path: z.string().min(1).describe("The absolute repository path of the file, for example '/src/example.ts'."),
582
+ content: z.string().describe("The complete UTF-8 text content to write to the file."),
583
+ commitMessage: z.string().min(1).describe("The commit message for the file change."),
584
+ expectedOldObjectId: z
585
+ .string()
586
+ .optional()
587
+ .describe("The expected current commit ID of the branch. When provided, the push fails if the branch has moved. If omitted, the current branch head is retrieved before pushing."),
588
+ }, async ({ action, repositoryId, project, branchName, path, content, commitMessage, expectedOldObjectId }) => {
589
+ try {
590
+ const connection = await connectionProvider();
591
+ const gitApi = await connection.getGitApi();
592
+ const refName = `refs/heads/${branchName}`;
593
+ const refs = await gitApi.getRefs(repositoryId, project, "heads/", false, false, undefined, false, undefined, branchName);
594
+ const branch = refs.find((ref) => ref.name === refName);
595
+ if (!branch?.objectId) {
596
+ return { content: [{ type: "text", text: `Error: Branch '${branchName}' not found in repository ${repositoryId}` }], isError: true };
597
+ }
598
+ if (expectedOldObjectId && expectedOldObjectId !== branch.objectId) {
599
+ return { content: [{ type: "text", text: `Error: Branch '${branchName}' has moved from expected commit ${expectedOldObjectId}` }], isError: true };
600
+ }
601
+ const oldObjectId = branch.objectId;
602
+ const filePath = path.startsWith("/") ? path : `/${path}`;
603
+ const push = await gitApi.createPush({
604
+ refUpdates: [{ name: refName, oldObjectId }],
605
+ commits: [
606
+ {
607
+ comment: commitMessage,
608
+ changes: [
609
+ {
610
+ changeType: action === "create" ? VersionControlChangeType.Add : VersionControlChangeType.Edit,
611
+ item: { path: filePath },
612
+ newContent: { content, contentType: ItemContentType.RawText },
613
+ },
614
+ ],
615
+ },
616
+ ],
617
+ }, repositoryId, project);
618
+ return {
619
+ content: [
620
+ {
621
+ type: "text",
622
+ text: JSON.stringify({ pushId: push.pushId, commitId: push.commits?.[0]?.commitId, branchName, path: filePath }, null, 2),
623
+ },
624
+ ],
625
+ };
626
+ }
627
+ catch (error) {
628
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
629
+ return { content: [{ type: "text", text: `Error writing repository file: ${errorMessage}` }], isError: true };
630
+ }
631
+ });
571
632
  // --- repo_search_commits ---------------------------------------------------
572
633
  server.tool(REPO_TOOLS.repo_search_commits, "Search commits with filtering by text, author, date range, and more.", {
573
634
  searchText: z.string().describe("Keywords to search for in commit messages"),
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.10.0-nightly.20260922";
1
+ export const packageVersion = "2.10.0-nightly.20260924";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.10.0-nightly.20260922",
3
+ "version": "2.10.0-nightly.20260924",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",