@mcp-consultant-tools/azure-devops 26.0.0-beta.1 → 27.0.0-beta.10
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/build/AzureDevOpsService.d.ts +179 -514
- package/build/AzureDevOpsService.d.ts.map +1 -1
- package/build/AzureDevOpsService.js +401 -994
- package/build/AzureDevOpsService.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1038 -844
- package/build/index.js.map +1 -1
- package/build/sync/file-utils.d.ts +86 -0
- package/build/sync/file-utils.d.ts.map +1 -0
- package/build/sync/file-utils.js +224 -0
- package/build/sync/file-utils.js.map +1 -0
- package/build/sync/git-utils.d.ts +31 -0
- package/build/sync/git-utils.d.ts.map +1 -0
- package/build/sync/git-utils.js +116 -0
- package/build/sync/git-utils.js.map +1 -0
- package/build/sync/html-converter.d.ts +32 -0
- package/build/sync/html-converter.d.ts.map +1 -0
- package/build/sync/html-converter.js +91 -0
- package/build/sync/html-converter.js.map +1 -0
- package/build/sync/html-detection.d.ts +93 -0
- package/build/sync/html-detection.d.ts.map +1 -0
- package/build/sync/html-detection.js +169 -0
- package/build/sync/html-detection.js.map +1 -0
- package/build/sync/index.d.ts +12 -0
- package/build/sync/index.d.ts.map +1 -0
- package/build/sync/index.js +12 -0
- package/build/sync/index.js.map +1 -0
- package/build/sync/markdown-serializer.d.ts +136 -0
- package/build/sync/markdown-serializer.d.ts.map +1 -0
- package/build/sync/markdown-serializer.js +646 -0
- package/build/sync/markdown-serializer.js.map +1 -0
- package/build/sync/task-serializer.d.ts +93 -0
- package/build/sync/task-serializer.d.ts.map +1 -0
- package/build/sync/task-serializer.js +395 -0
- package/build/sync/task-serializer.js.map +1 -0
- package/build/tool-examples.d.ts +56 -0
- package/build/tool-examples.d.ts.map +1 -0
- package/build/tool-examples.js +142 -0
- package/build/tool-examples.js.map +1 -0
- package/package.json +5 -2
package/build/index.js
CHANGED
|
@@ -10,6 +10,14 @@ import { realpathSync } from "node:fs";
|
|
|
10
10
|
import { createMcpServer, createEnvLoader } from "@mcp-consultant-tools/core";
|
|
11
11
|
import { AzureDevOpsService } from "./AzureDevOpsService.js";
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
+
import { descWithExamples, WIQL_EXAMPLES, PATCH_OP_EXAMPLES, WORK_ITEM_FIELD_EXAMPLES, SYNC_TO_FILE_EXAMPLES, SYNC_FROM_FILE_EXAMPLES, SYNC_TASKS_TO_FILE_EXAMPLES, SYNC_TASKS_FROM_FILE_EXAMPLES, PR_BRANCH_REF_EXAMPLES, PR_MERGE_STRATEGY_EXAMPLES, PR_VOTE_EXAMPLES, } from './tool-examples.js';
|
|
14
|
+
import { checkFieldFormats, workItemToMarkdown, commentsToMarkdown, buildPatchOperations, updateSyncRevision, getSyncConfig, ensureFolderExists, getWorkItemFilePath, getCommentsFilePath, fileExists, writeWorkItemFile, readWorkItemFile, readFileContent, listSyncedWorkItems, validateFolderPath, autoCommitMultipleFiles,
|
|
15
|
+
// Task sync utilities
|
|
16
|
+
tasksToMarkdown, parseTasksMarkdown, buildTaskPatchOperations, buildNewTaskFields, getTasksFilePath, updateTasksFileAfterCreate,
|
|
17
|
+
// New work item utilities
|
|
18
|
+
parseNewWorkItemMarkdown, buildNewWorkItemFields, generateNewWorkItemTemplate, convertNewFileToSynced, getNewWorkItemFilePath, findNextNewFileIndex, findNewWorkItemFiles, renameFile,
|
|
19
|
+
// Markdown format utilities
|
|
20
|
+
getAllLargeTextFields, autoConvertFieldsToMarkdown, isHtmlContent, } from './sync/index.js';
|
|
13
21
|
/**
|
|
14
22
|
* Register azure-devops tools and prompts to an MCP server
|
|
15
23
|
* @param server - The MCP server instance
|
|
@@ -38,23 +46,9 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
38
46
|
enableWorkItemWrite: process.env.AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE === "true",
|
|
39
47
|
enableWorkItemDelete: process.env.AZUREDEVOPS_ENABLE_WORK_ITEM_DELETE === "true",
|
|
40
48
|
enableWikiWrite: process.env.AZUREDEVOPS_ENABLE_WIKI_WRITE === "true",
|
|
49
|
+
enablePullRequestWrite: process.env.AZUREDEVOPS_ENABLE_PR_WRITE === "true",
|
|
41
50
|
// Comment format: 'markdown' (default) or 'html' (for legacy orgs without Markdown preview)
|
|
42
51
|
commentFormat: process.env.AZUREDEVOPS_COMMENT_FORMAT || 'markdown',
|
|
43
|
-
// DevOps Admin Tools - Three-tier permission model
|
|
44
|
-
// Tier 1: Read-only (master switch)
|
|
45
|
-
showDevOpsAdminReadonly: process.env.AZUREDEVOPS_SHOW_DEVOPS_ADMIN_READONLY === "true",
|
|
46
|
-
// Tier 2: Upsert (create + update)
|
|
47
|
-
enablePipelineUpsert: process.env.AZUREDEVOPS_ENABLE_PIPELINE_UPSERT === "true",
|
|
48
|
-
enableServiceConnUpsert: process.env.AZUREDEVOPS_ENABLE_SERVICE_CONN_UPSERT === "true",
|
|
49
|
-
enableVariableGroupUpsert: process.env.AZUREDEVOPS_ENABLE_VARIABLE_GROUP_UPSERT === "true",
|
|
50
|
-
enableAgentPoolUpsert: process.env.AZUREDEVOPS_ENABLE_AGENT_POOL_UPSERT === "true",
|
|
51
|
-
enableEnvironmentUpsert: process.env.AZUREDEVOPS_ENABLE_ENVIRONMENT_UPSERT === "true",
|
|
52
|
-
// Tier 3: Delete/Disable (destructive operations)
|
|
53
|
-
enablePipelineDelete: process.env.AZUREDEVOPS_ENABLE_PIPELINE_DELETE === "true",
|
|
54
|
-
enableServiceConnDelete: process.env.AZUREDEVOPS_ENABLE_SERVICE_CONN_DELETE === "true",
|
|
55
|
-
enableVariableGroupDelete: process.env.AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE === "true",
|
|
56
|
-
enableAgentPoolDisable: process.env.AZUREDEVOPS_ENABLE_AGENT_POOL_DISABLE === "true",
|
|
57
|
-
enableEnvironmentDelete: process.env.AZUREDEVOPS_ENABLE_ENVIRONMENT_DELETE === "true",
|
|
58
52
|
};
|
|
59
53
|
service = new AzureDevOpsService(config);
|
|
60
54
|
console.error("Azure DevOps service initialized");
|
|
@@ -330,6 +324,45 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
330
324
|
// ========================================
|
|
331
325
|
// TOOLS
|
|
332
326
|
// ========================================
|
|
327
|
+
server.tool("get-configuration", "Get the configured Azure DevOps organization and projects. Use this to construct correct URLs.", {}, async () => {
|
|
328
|
+
try {
|
|
329
|
+
const organization = process.env.AZUREDEVOPS_ORGANIZATION;
|
|
330
|
+
const projects = process.env.AZUREDEVOPS_PROJECTS?.split(",").map(p => p.trim()).filter(p => p) || [];
|
|
331
|
+
const syncFolder = process.env.AZUREDEVOPS_SYNC_FOLDER || 'docs/user-stories';
|
|
332
|
+
if (!organization || projects.length === 0) {
|
|
333
|
+
return {
|
|
334
|
+
content: [{
|
|
335
|
+
type: "text",
|
|
336
|
+
text: "Azure DevOps not configured. Set AZUREDEVOPS_ORGANIZATION and AZUREDEVOPS_PROJECTS environment variables.",
|
|
337
|
+
}],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const config = {
|
|
341
|
+
organization,
|
|
342
|
+
projects,
|
|
343
|
+
syncFolder,
|
|
344
|
+
urlPatterns: {
|
|
345
|
+
workItem: `https://dev.azure.com/${organization}/{project}/_workitems/edit/{id}`,
|
|
346
|
+
pullRequest: `https://dev.azure.com/${organization}/{project}/_git/{repo}/pullrequest/{id}`,
|
|
347
|
+
wiki: `https://dev.azure.com/${organization}/{project}/_wiki/wikis/{wikiName}/{pagePath}`,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
content: [{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: `Azure DevOps Configuration:\n\n${JSON.stringify(config, null, 2)}`,
|
|
354
|
+
}],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return {
|
|
359
|
+
content: [{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: `Failed to get configuration: ${error.message}`,
|
|
362
|
+
}],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
333
366
|
server.tool("get-wikis", "Get all wikis in an Azure DevOps project", {
|
|
334
367
|
project: z.string().describe("The project name"),
|
|
335
368
|
}, async ({ project }) => {
|
|
@@ -483,7 +516,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
483
516
|
};
|
|
484
517
|
}
|
|
485
518
|
});
|
|
486
|
-
server.tool("
|
|
519
|
+
server.tool("ado-str-replace-wiki", "Replace a specific string in an Azure DevOps wiki page without rewriting entire content. More efficient than update-wiki-page for small changes. (requires AZUREDEVOPS_ENABLE_WIKI_WRITE=true)", {
|
|
487
520
|
project: z.string().describe("The project name"),
|
|
488
521
|
wikiId: z.string().describe("The wiki identifier (ID or name)"),
|
|
489
522
|
pagePath: z.string().describe("The path to the wiki page (e.g., '/SharePoint-Online/04-DEV-Configuration')"),
|
|
@@ -549,7 +582,7 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
549
582
|
});
|
|
550
583
|
server.tool("query-work-items", "Query work items using WIQL (Work Item Query Language) in Azure DevOps", {
|
|
551
584
|
project: z.string().describe("The project name"),
|
|
552
|
-
wiql: z.string().describe("The WIQL query string
|
|
585
|
+
wiql: z.string().describe(descWithExamples("The WIQL query string. SQL-like syntax with field names in brackets. Common fields: [System.Id], [System.Title], [System.State], [System.WorkItemType], [System.AssignedTo], [System.Parent]. Use @Me for current user.", WIQL_EXAMPLES)),
|
|
553
586
|
maxResults: z.number().optional().describe("Maximum number of results (default: 200)"),
|
|
554
587
|
}, async ({ project, wiql, maxResults }) => {
|
|
555
588
|
try {
|
|
@@ -636,24 +669,53 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
636
669
|
};
|
|
637
670
|
}
|
|
638
671
|
});
|
|
639
|
-
server.tool("update-work-item", "Update a work item in Azure DevOps using JSON Patch operations (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true)", {
|
|
672
|
+
server.tool("update-work-item", "Update a work item in Azure DevOps using JSON Patch operations (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true). Auto-injects markdown format operations for large text fields.", {
|
|
640
673
|
project: z.string().describe("The project name"),
|
|
641
674
|
workItemId: z.number().describe("The work item ID"),
|
|
642
675
|
patchOperations: z.array(z.object({
|
|
643
|
-
op: z.string().describe("The operation type
|
|
644
|
-
path: z.string().describe("The field path (e.g., '/fields/System.State')"),
|
|
676
|
+
op: z.string().describe("The operation type: 'add' (set value), 'replace' (update existing), or 'remove' (clear field)"),
|
|
677
|
+
path: z.string().describe("The field path starting with '/fields/' (e.g., '/fields/System.State', '/fields/System.Title')"),
|
|
645
678
|
value: z.any().optional().describe("The value to set (not required for 'remove' operation)")
|
|
646
|
-
})).describe("Array of JSON Patch operations"),
|
|
647
|
-
|
|
679
|
+
})).describe(descWithExamples("Array of JSON Patch operations. Each operation specifies what to change.", PATCH_OP_EXAMPLES)),
|
|
680
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic markdown format injection for large text fields. Only use when explicitly requested. Default: false"),
|
|
681
|
+
}, async ({ project, workItemId, patchOperations, skipAutoConvert }) => {
|
|
648
682
|
try {
|
|
649
683
|
const service = getAzureDevOpsService();
|
|
650
|
-
|
|
684
|
+
// Auto-inject markdown format operations for large text fields
|
|
685
|
+
let finalOperations = [...patchOperations];
|
|
686
|
+
const formatOpsAdded = [];
|
|
687
|
+
if (!skipAutoConvert) {
|
|
688
|
+
// Find large text fields being updated (includes custom fields)
|
|
689
|
+
const allLargeTextFields = getAllLargeTextFields();
|
|
690
|
+
const largeTextFieldsBeingUpdated = patchOperations
|
|
691
|
+
.filter((op) => op.path?.startsWith('/fields/'))
|
|
692
|
+
.map((op) => op.path.replace('/fields/', ''))
|
|
693
|
+
.filter((field) => allLargeTextFields.includes(field));
|
|
694
|
+
for (const field of largeTextFieldsBeingUpdated) {
|
|
695
|
+
// Check if format operation already exists
|
|
696
|
+
const hasFormatOp = finalOperations.some((op) => op.path === `/multilineFieldsFormat/${field}`);
|
|
697
|
+
if (!hasFormatOp) {
|
|
698
|
+
// Add format operation to ensure markdown
|
|
699
|
+
finalOperations.push({
|
|
700
|
+
op: 'add',
|
|
701
|
+
path: `/multilineFieldsFormat/${field}`,
|
|
702
|
+
value: 'Markdown'
|
|
703
|
+
});
|
|
704
|
+
formatOpsAdded.push(field);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const result = await service.updateWorkItem(project, workItemId, finalOperations);
|
|
709
|
+
let message = `Updated work item ${workItemId}`;
|
|
710
|
+
if (formatOpsAdded.length > 0) {
|
|
711
|
+
message += ` (auto-set markdown format for: ${formatOpsAdded.join(', ')})`;
|
|
712
|
+
}
|
|
651
713
|
const resultStr = JSON.stringify(result, null, 2);
|
|
652
714
|
return {
|
|
653
715
|
content: [
|
|
654
716
|
{
|
|
655
717
|
type: "text",
|
|
656
|
-
text:
|
|
718
|
+
text: `${message}:\n\n${resultStr}`,
|
|
657
719
|
},
|
|
658
720
|
],
|
|
659
721
|
};
|
|
@@ -672,8 +734,8 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
672
734
|
});
|
|
673
735
|
server.tool("create-work-item", "Create a new work item in Azure DevOps with optional parent relationship (requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true)", {
|
|
674
736
|
project: z.string().describe("The project name"),
|
|
675
|
-
workItemType: z.string().describe("The work item type
|
|
676
|
-
fields: z.record(z.any()).describe("Object with field values
|
|
737
|
+
workItemType: z.string().describe("The work item type: 'Bug', 'Task', 'User Story', 'Feature', 'Epic', or custom types"),
|
|
738
|
+
fields: z.record(z.any()).describe(descWithExamples("Object with field values. Required: System.Title. Common fields: System.Description, System.State, System.AssignedTo, Microsoft.VSTS.Common.AcceptanceCriteria, Microsoft.VSTS.TCM.ReproSteps (bugs).", WORK_ITEM_FIELD_EXAMPLES)),
|
|
677
739
|
parentId: z.number().optional().describe("Optional parent work item ID (for creating child items). Simplified alternative to relations parameter."),
|
|
678
740
|
relations: z.array(z.object({
|
|
679
741
|
rel: z.string().describe("Relation type (e.g., 'System.LinkTypes.Hierarchy-Reverse' for parent)"),
|
|
@@ -736,900 +798,1032 @@ export function registerAzureDevOpsTools(server, azuredevopsService) {
|
|
|
736
798
|
}
|
|
737
799
|
});
|
|
738
800
|
// ========================================
|
|
739
|
-
//
|
|
801
|
+
// PULL REQUEST TOOLS (Read-only - always available)
|
|
740
802
|
// ========================================
|
|
741
|
-
server.tool("list-
|
|
803
|
+
server.tool("list-repositories", "List all Git repositories in an Azure DevOps project. Returns repository ID, name, default branch, and URLs.", {
|
|
742
804
|
project: z.string().describe("The project name"),
|
|
743
805
|
}, async ({ project }) => {
|
|
744
806
|
try {
|
|
745
807
|
const service = getAzureDevOpsService();
|
|
746
|
-
const result = await service.
|
|
747
|
-
|
|
748
|
-
return {
|
|
749
|
-
content: [
|
|
750
|
-
{
|
|
751
|
-
type: "text",
|
|
752
|
-
text: `Variable groups in project '${project}':\n\n${resultStr}`,
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
};
|
|
808
|
+
const result = await service.listRepositories(project);
|
|
809
|
+
return { content: [{ type: "text", text: `Repositories in project '${project}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
756
810
|
}
|
|
757
811
|
catch (error) {
|
|
758
|
-
console.error("Error listing
|
|
759
|
-
return {
|
|
760
|
-
content: [
|
|
761
|
-
{
|
|
762
|
-
type: "text",
|
|
763
|
-
text: `Failed to list variable groups: ${error.message}`,
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
};
|
|
812
|
+
console.error("Error listing repositories:", error);
|
|
813
|
+
return { content: [{ type: "text", text: `Failed to list repositories: ${error.message}` }] };
|
|
767
814
|
}
|
|
768
815
|
});
|
|
769
|
-
server.tool("
|
|
816
|
+
server.tool("list-pull-requests", "List pull requests in a Git repository. Filter by status (active, completed, abandoned, all). Returns PR ID, title, author, branches, and review status.", {
|
|
770
817
|
project: z.string().describe("The project name"),
|
|
771
|
-
|
|
772
|
-
|
|
818
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
819
|
+
status: z.enum(["active", "completed", "abandoned", "all"]).optional().describe("Filter by PR status (default: active)"),
|
|
820
|
+
top: z.number().optional().describe("Maximum results (default: 25)"),
|
|
821
|
+
}, async ({ project, repositoryId, status, top }) => {
|
|
773
822
|
try {
|
|
774
823
|
const service = getAzureDevOpsService();
|
|
775
|
-
const result = await service.
|
|
776
|
-
|
|
777
|
-
return {
|
|
778
|
-
content: [
|
|
779
|
-
{
|
|
780
|
-
type: "text",
|
|
781
|
-
text: `Variable group ${groupId} in project '${project}':\n\n${resultStr}`,
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
};
|
|
824
|
+
const result = await service.listPullRequests(project, repositoryId, status || 'active', top || 25);
|
|
825
|
+
return { content: [{ type: "text", text: `Pull requests in '${repositoryId}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
785
826
|
}
|
|
786
827
|
catch (error) {
|
|
787
|
-
console.error("Error
|
|
788
|
-
return {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
828
|
+
console.error("Error listing pull requests:", error);
|
|
829
|
+
return { content: [{ type: "text", text: `Failed to list pull requests: ${error.message}` }] };
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
server.tool("get-pull-request", "Get details of a specific pull request including title, description, author, reviewers with votes, and merge status.", {
|
|
833
|
+
project: z.string().describe("The project name"),
|
|
834
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
835
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
836
|
+
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
837
|
+
try {
|
|
838
|
+
const service = getAzureDevOpsService();
|
|
839
|
+
const result = await service.getPullRequest(project, repositoryId, pullRequestId);
|
|
840
|
+
return { content: [{ type: "text", text: `Pull request #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
console.error("Error getting pull request:", error);
|
|
844
|
+
return { content: [{ type: "text", text: `Failed to get pull request: ${error.message}` }] };
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
server.tool("get-pull-request-threads", "Get all comment threads and discussions on a pull request. Includes inline code comments with file paths and line numbers.", {
|
|
848
|
+
project: z.string().describe("The project name"),
|
|
849
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
850
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
851
|
+
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
852
|
+
try {
|
|
853
|
+
const service = getAzureDevOpsService();
|
|
854
|
+
const result = await service.getPullRequestThreads(project, repositoryId, pullRequestId);
|
|
855
|
+
return { content: [{ type: "text", text: `Threads for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.error("Error getting pull request threads:", error);
|
|
859
|
+
return { content: [{ type: "text", text: `Failed to get pull request threads: ${error.message}` }] };
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
server.tool("get-pull-request-commits", "Get all commits included in a pull request. Shows commit ID, message, author, and date.", {
|
|
863
|
+
project: z.string().describe("The project name"),
|
|
864
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
865
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
866
|
+
}, async ({ project, repositoryId, pullRequestId }) => {
|
|
867
|
+
try {
|
|
868
|
+
const service = getAzureDevOpsService();
|
|
869
|
+
const result = await service.getPullRequestCommits(project, repositoryId, pullRequestId);
|
|
870
|
+
return { content: [{ type: "text", text: `Commits for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
console.error("Error getting pull request commits:", error);
|
|
874
|
+
return { content: [{ type: "text", text: `Failed to get pull request commits: ${error.message}` }] };
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
server.tool("get-pull-request-changes", "Get file changes (diffs) in a pull request. Shows added, modified, deleted, and renamed files with their paths.", {
|
|
878
|
+
project: z.string().describe("The project name"),
|
|
879
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
880
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
881
|
+
iterationId: z.number().optional().describe("Iteration ID (default: latest)"),
|
|
882
|
+
}, async ({ project, repositoryId, pullRequestId, iterationId }) => {
|
|
883
|
+
try {
|
|
884
|
+
const service = getAzureDevOpsService();
|
|
885
|
+
const result = await service.getPullRequestChanges(project, repositoryId, pullRequestId, iterationId);
|
|
886
|
+
return { content: [{ type: "text", text: `Changes for PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
console.error("Error getting pull request changes:", error);
|
|
890
|
+
return { content: [{ type: "text", text: `Failed to get pull request changes: ${error.message}` }] };
|
|
796
891
|
}
|
|
797
892
|
});
|
|
798
893
|
// ========================================
|
|
799
|
-
//
|
|
800
|
-
// Tools are only registered when corresponding flags are enabled
|
|
801
|
-
// ========================================
|
|
802
|
-
// Helper to check if admin readonly is enabled (master switch)
|
|
803
|
-
const showAdminReadonly = process.env.AZUREDEVOPS_SHOW_DEVOPS_ADMIN_READONLY === "true";
|
|
804
|
-
// Tier 2: Upsert flags
|
|
805
|
-
const enablePipelineUpsert = process.env.AZUREDEVOPS_ENABLE_PIPELINE_UPSERT === "true";
|
|
806
|
-
const enableServiceConnUpsert = process.env.AZUREDEVOPS_ENABLE_SERVICE_CONN_UPSERT === "true";
|
|
807
|
-
const enableVariableGroupUpsert = process.env.AZUREDEVOPS_ENABLE_VARIABLE_GROUP_UPSERT === "true";
|
|
808
|
-
const enableAgentPoolUpsert = process.env.AZUREDEVOPS_ENABLE_AGENT_POOL_UPSERT === "true";
|
|
809
|
-
const enableEnvironmentUpsert = process.env.AZUREDEVOPS_ENABLE_ENVIRONMENT_UPSERT === "true";
|
|
810
|
-
// Tier 3: Delete/Disable flags
|
|
811
|
-
const enablePipelineDelete = process.env.AZUREDEVOPS_ENABLE_PIPELINE_DELETE === "true";
|
|
812
|
-
const enableServiceConnDelete = process.env.AZUREDEVOPS_ENABLE_SERVICE_CONN_DELETE === "true";
|
|
813
|
-
const enableVariableGroupDelete = process.env.AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE === "true";
|
|
814
|
-
const enableAgentPoolDisable = process.env.AZUREDEVOPS_ENABLE_AGENT_POOL_DISABLE === "true";
|
|
815
|
-
const enableEnvironmentDelete = process.env.AZUREDEVOPS_ENABLE_ENVIRONMENT_DELETE === "true";
|
|
816
|
-
// Track registered tools count
|
|
817
|
-
let adminToolsCount = 0;
|
|
818
|
-
// ========================================
|
|
819
|
-
// PIPELINE READ-ONLY TOOLS (Tier 1)
|
|
820
|
-
// ========================================
|
|
821
|
-
if (showAdminReadonly) {
|
|
822
|
-
server.tool("list-pipelines", "List all YAML pipeline definitions in an Azure DevOps project. Returns pipeline ID, name, path, repository, and YAML file path.", {
|
|
823
|
-
project: z.string().describe("The project name"),
|
|
824
|
-
}, async ({ project }) => {
|
|
825
|
-
try {
|
|
826
|
-
const service = getAzureDevOpsService();
|
|
827
|
-
const result = await service.listPipelineDefinitions(project);
|
|
828
|
-
return { content: [{ type: "text", text: `Pipeline definitions in project '${project}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
829
|
-
}
|
|
830
|
-
catch (error) {
|
|
831
|
-
console.error("Error listing pipelines:", error);
|
|
832
|
-
return { content: [{ type: "text", text: `Failed to list pipelines: ${error.message}` }] };
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
adminToolsCount++;
|
|
836
|
-
server.tool("get-pipeline-definition", "Get detailed YAML pipeline definition including triggers, variables (secrets masked), queue settings, and repository configuration.", {
|
|
837
|
-
project: z.string().describe("The project name"),
|
|
838
|
-
definitionId: z.number().describe("The pipeline definition ID"),
|
|
839
|
-
}, async ({ project, definitionId }) => {
|
|
840
|
-
try {
|
|
841
|
-
const service = getAzureDevOpsService();
|
|
842
|
-
const result = await service.getPipelineDefinition(project, definitionId);
|
|
843
|
-
return { content: [{ type: "text", text: `Pipeline definition ${definitionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
console.error("Error getting pipeline definition:", error);
|
|
847
|
-
return { content: [{ type: "text", text: `Failed to get pipeline definition: ${error.message}` }] };
|
|
848
|
-
}
|
|
849
|
-
});
|
|
850
|
-
adminToolsCount++;
|
|
851
|
-
server.tool("get-pipeline-yaml", "Get the YAML content for a pipeline definition. Returns the raw azure-pipelines.yml content.", {
|
|
852
|
-
project: z.string().describe("The project name"),
|
|
853
|
-
definitionId: z.number().describe("The pipeline definition ID"),
|
|
854
|
-
}, async ({ project, definitionId }) => {
|
|
855
|
-
try {
|
|
856
|
-
const service = getAzureDevOpsService();
|
|
857
|
-
const result = await service.getPipelineYaml(project, definitionId);
|
|
858
|
-
return { content: [{ type: "text", text: `Pipeline YAML for definition ${definitionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
859
|
-
}
|
|
860
|
-
catch (error) {
|
|
861
|
-
console.error("Error getting pipeline YAML:", error);
|
|
862
|
-
return { content: [{ type: "text", text: `Failed to get pipeline YAML: ${error.message}` }] };
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
adminToolsCount++;
|
|
866
|
-
server.tool("list-pipeline-runs", "List recent pipeline runs for a definition. Returns build ID, status, result, branch, timestamps, and who triggered it.", {
|
|
867
|
-
project: z.string().describe("The project name"),
|
|
868
|
-
definitionId: z.number().describe("The pipeline definition ID"),
|
|
869
|
-
top: z.number().optional().describe("Maximum number of results (default: 10)"),
|
|
870
|
-
}, async ({ project, definitionId, top }) => {
|
|
871
|
-
try {
|
|
872
|
-
const service = getAzureDevOpsService();
|
|
873
|
-
const result = await service.listPipelineRuns(project, definitionId, top || 10);
|
|
874
|
-
return { content: [{ type: "text", text: `Recent runs for pipeline ${definitionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
875
|
-
}
|
|
876
|
-
catch (error) {
|
|
877
|
-
console.error("Error listing pipeline runs:", error);
|
|
878
|
-
return { content: [{ type: "text", text: `Failed to list pipeline runs: ${error.message}` }] };
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
adminToolsCount++;
|
|
882
|
-
server.tool("get-build-status", "Get build status and details. Use detail='summary' for basic status, 'timeline' for step breakdown (default scope='problems' shows only failed/warning items), or 'full' for logs. The timelineScope controls what records are included: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything).", {
|
|
883
|
-
project: z.string().describe("The project name"),
|
|
884
|
-
buildId: z.number().describe("The build ID"),
|
|
885
|
-
detail: z.enum(["summary", "timeline", "full"]).optional().describe("Level of detail: 'summary' (default), 'timeline' (include steps), or 'full' (include logs)"),
|
|
886
|
-
timelineScope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Timeline scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
887
|
-
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
888
|
-
}, async ({ project, buildId, detail, timelineScope, maxIssues }) => {
|
|
889
|
-
try {
|
|
890
|
-
const service = getAzureDevOpsService();
|
|
891
|
-
const result = await service.getBuildStatus(project, buildId, detail || 'summary', timelineScope || 'problems', maxIssues || 5);
|
|
892
|
-
return { content: [{ type: "text", text: `Build ${buildId} status:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
893
|
-
}
|
|
894
|
-
catch (error) {
|
|
895
|
-
console.error("Error getting build status:", error);
|
|
896
|
-
return { content: [{ type: "text", text: `Failed to get build status: ${error.message}` }] };
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
adminToolsCount++;
|
|
900
|
-
server.tool("get-build-timeline", "Get step-by-step breakdown of a build. Shows stages, jobs, and tasks with timing, status, and error/warning counts. Use scope to control output size: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything). Always includes summary stats regardless of scope.", {
|
|
901
|
-
project: z.string().describe("The project name"),
|
|
902
|
-
buildId: z.number().describe("The build ID"),
|
|
903
|
-
scope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Filter scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
904
|
-
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
905
|
-
}, async ({ project, buildId, scope, maxIssues }) => {
|
|
906
|
-
try {
|
|
907
|
-
const service = getAzureDevOpsService();
|
|
908
|
-
const result = await service.getBuildTimeline(project, buildId, scope || 'problems', maxIssues || 5);
|
|
909
|
-
return { content: [{ type: "text", text: `Build ${buildId} timeline:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
910
|
-
}
|
|
911
|
-
catch (error) {
|
|
912
|
-
console.error("Error getting build timeline:", error);
|
|
913
|
-
return { content: [{ type: "text", text: `Failed to get build timeline: ${error.message}` }] };
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
adminToolsCount++;
|
|
917
|
-
server.tool("get-build-logs", "Get build logs. Without logId, returns list of available logs with line counts. With logId, returns that log's content. NOTE: For verbose logs, consider using a sub-agent to analyze the content.", {
|
|
918
|
-
project: z.string().describe("The project name"),
|
|
919
|
-
buildId: z.number().describe("The build ID"),
|
|
920
|
-
logId: z.number().optional().describe("Optional specific log ID to retrieve content"),
|
|
921
|
-
}, async ({ project, buildId, logId }) => {
|
|
922
|
-
try {
|
|
923
|
-
const service = getAzureDevOpsService();
|
|
924
|
-
const result = await service.getBuildLogs(project, buildId, logId);
|
|
925
|
-
return { content: [{ type: "text", text: `Build ${buildId} logs:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
926
|
-
}
|
|
927
|
-
catch (error) {
|
|
928
|
-
console.error("Error getting build logs:", error);
|
|
929
|
-
return { content: [{ type: "text", text: `Failed to get build logs: ${error.message}` }] };
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
adminToolsCount++;
|
|
933
|
-
// ========================================
|
|
934
|
-
// SERVICE CONNECTION READ-ONLY TOOLS (Tier 1)
|
|
935
|
-
// ========================================
|
|
936
|
-
server.tool("list-service-connections", "List all service connections in a project. Shows connection type, URL, authorization scheme, and sharing status. Credentials are masked.", {
|
|
937
|
-
project: z.string().describe("The project name"),
|
|
938
|
-
}, async ({ project }) => {
|
|
939
|
-
try {
|
|
940
|
-
const service = getAzureDevOpsService();
|
|
941
|
-
const result = await service.listServiceConnections(project);
|
|
942
|
-
return { content: [{ type: "text", text: `Service connections in project '${project}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
943
|
-
}
|
|
944
|
-
catch (error) {
|
|
945
|
-
console.error("Error listing service connections:", error);
|
|
946
|
-
return { content: [{ type: "text", text: `Failed to list service connections: ${error.message}` }] };
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
adminToolsCount++;
|
|
950
|
-
server.tool("get-service-connection", "Get detailed service connection configuration. Returns type, URL, authorization scheme, data fields, and project references. Secrets are masked.", {
|
|
951
|
-
project: z.string().describe("The project name"),
|
|
952
|
-
connectionId: z.string().describe("The service connection ID (GUID)"),
|
|
953
|
-
}, async ({ project, connectionId }) => {
|
|
954
|
-
try {
|
|
955
|
-
const service = getAzureDevOpsService();
|
|
956
|
-
const result = await service.getServiceConnection(project, connectionId);
|
|
957
|
-
return { content: [{ type: "text", text: `Service connection ${connectionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
958
|
-
}
|
|
959
|
-
catch (error) {
|
|
960
|
-
console.error("Error getting service connection:", error);
|
|
961
|
-
return { content: [{ type: "text", text: `Failed to get service connection: ${error.message}` }] };
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
adminToolsCount++;
|
|
965
|
-
server.tool("get-service-connection-types", "Get all available service connection types (Azure, AWS, Docker, GitHub, etc.) with their authentication schemes and configuration options.", {}, async () => {
|
|
966
|
-
try {
|
|
967
|
-
const service = getAzureDevOpsService();
|
|
968
|
-
const result = await service.getServiceConnectionTypes();
|
|
969
|
-
return { content: [{ type: "text", text: `Available service connection types:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
console.error("Error getting service connection types:", error);
|
|
973
|
-
return { content: [{ type: "text", text: `Failed to get service connection types: ${error.message}` }] };
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
adminToolsCount++;
|
|
977
|
-
// ========================================
|
|
978
|
-
// AGENT POOL READ-ONLY TOOLS (Tier 1)
|
|
979
|
-
// ========================================
|
|
980
|
-
server.tool("list-agent-pools", "List all agent pools in the organization. Shows pool type (automation/deployment), size, hosted status, and auto-provision settings.", {
|
|
981
|
-
poolType: z.enum(["automation", "deployment"]).optional().describe("Optional filter: 'automation' or 'deployment'"),
|
|
982
|
-
}, async ({ poolType }) => {
|
|
983
|
-
try {
|
|
984
|
-
const service = getAzureDevOpsService();
|
|
985
|
-
const result = await service.listAgentPools(poolType);
|
|
986
|
-
return { content: [{ type: "text", text: `Agent pools:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
987
|
-
}
|
|
988
|
-
catch (error) {
|
|
989
|
-
console.error("Error listing agent pools:", error);
|
|
990
|
-
return { content: [{ type: "text", text: `Failed to list agent pools: ${error.message}` }] };
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
adminToolsCount++;
|
|
994
|
-
server.tool("get-agent-pool", "Get detailed agent pool configuration including auto-provision, auto-update, auto-size settings, and owner information.", {
|
|
995
|
-
poolId: z.number().describe("The agent pool ID"),
|
|
996
|
-
}, async ({ poolId }) => {
|
|
997
|
-
try {
|
|
998
|
-
const service = getAzureDevOpsService();
|
|
999
|
-
const result = await service.getAgentPool(poolId);
|
|
1000
|
-
return { content: [{ type: "text", text: `Agent pool ${poolId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1001
|
-
}
|
|
1002
|
-
catch (error) {
|
|
1003
|
-
console.error("Error getting agent pool:", error);
|
|
1004
|
-
return { content: [{ type: "text", text: `Failed to get agent pool: ${error.message}` }] };
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
adminToolsCount++;
|
|
1008
|
-
server.tool("list-agents", "List all agents in a pool. Shows agent name, version, OS, enabled status, and current status (online/offline).", {
|
|
1009
|
-
poolId: z.number().describe("The agent pool ID"),
|
|
1010
|
-
includeCapabilities: z.boolean().optional().describe("Include system and user capabilities (default: false)"),
|
|
1011
|
-
}, async ({ poolId, includeCapabilities }) => {
|
|
1012
|
-
try {
|
|
1013
|
-
const service = getAzureDevOpsService();
|
|
1014
|
-
const result = await service.listAgents(poolId, includeCapabilities || false);
|
|
1015
|
-
return { content: [{ type: "text", text: `Agents in pool ${poolId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1016
|
-
}
|
|
1017
|
-
catch (error) {
|
|
1018
|
-
console.error("Error listing agents:", error);
|
|
1019
|
-
return { content: [{ type: "text", text: `Failed to list agents: ${error.message}` }] };
|
|
1020
|
-
}
|
|
1021
|
-
});
|
|
1022
|
-
adminToolsCount++;
|
|
1023
|
-
server.tool("get-agent", "Get detailed agent information including capabilities, current assignment, and last completed request.", {
|
|
1024
|
-
poolId: z.number().describe("The agent pool ID"),
|
|
1025
|
-
agentId: z.number().describe("The agent ID"),
|
|
1026
|
-
}, async ({ poolId, agentId }) => {
|
|
1027
|
-
try {
|
|
1028
|
-
const service = getAzureDevOpsService();
|
|
1029
|
-
const result = await service.getAgent(poolId, agentId);
|
|
1030
|
-
return { content: [{ type: "text", text: `Agent ${agentId} in pool ${poolId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1031
|
-
}
|
|
1032
|
-
catch (error) {
|
|
1033
|
-
console.error("Error getting agent:", error);
|
|
1034
|
-
return { content: [{ type: "text", text: `Failed to get agent: ${error.message}` }] };
|
|
1035
|
-
}
|
|
1036
|
-
});
|
|
1037
|
-
adminToolsCount++;
|
|
1038
|
-
// ========================================
|
|
1039
|
-
// ENVIRONMENT READ-ONLY TOOLS (Tier 1)
|
|
1040
|
-
// ========================================
|
|
1041
|
-
server.tool("list-environments", "List all deployment environments in a project. Shows environment name, description, and modification info.", {
|
|
1042
|
-
project: z.string().describe("The project name"),
|
|
1043
|
-
}, async ({ project }) => {
|
|
1044
|
-
try {
|
|
1045
|
-
const service = getAzureDevOpsService();
|
|
1046
|
-
const result = await service.listEnvironments(project);
|
|
1047
|
-
return { content: [{ type: "text", text: `Environments in project '${project}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1048
|
-
}
|
|
1049
|
-
catch (error) {
|
|
1050
|
-
console.error("Error listing environments:", error);
|
|
1051
|
-
return { content: [{ type: "text", text: `Failed to list environments: ${error.message}` }] };
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
adminToolsCount++;
|
|
1055
|
-
server.tool("get-environment", "Get detailed environment configuration including associated resources (Kubernetes, VMs, etc.).", {
|
|
1056
|
-
project: z.string().describe("The project name"),
|
|
1057
|
-
environmentId: z.number().describe("The environment ID"),
|
|
1058
|
-
}, async ({ project, environmentId }) => {
|
|
1059
|
-
try {
|
|
1060
|
-
const service = getAzureDevOpsService();
|
|
1061
|
-
const result = await service.getEnvironment(project, environmentId);
|
|
1062
|
-
return { content: [{ type: "text", text: `Environment ${environmentId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1063
|
-
}
|
|
1064
|
-
catch (error) {
|
|
1065
|
-
console.error("Error getting environment:", error);
|
|
1066
|
-
return { content: [{ type: "text", text: `Failed to get environment: ${error.message}` }] };
|
|
1067
|
-
}
|
|
1068
|
-
});
|
|
1069
|
-
adminToolsCount++;
|
|
1070
|
-
server.tool("get-environment-deployments", "Get deployment history for an environment. Shows pipeline, owner, start/finish times, and result.", {
|
|
1071
|
-
project: z.string().describe("The project name"),
|
|
1072
|
-
environmentId: z.number().describe("The environment ID"),
|
|
1073
|
-
top: z.number().optional().describe("Maximum number of results (default: 10)"),
|
|
1074
|
-
}, async ({ project, environmentId, top }) => {
|
|
1075
|
-
try {
|
|
1076
|
-
const service = getAzureDevOpsService();
|
|
1077
|
-
const result = await service.getEnvironmentDeployments(project, environmentId, top || 10);
|
|
1078
|
-
return { content: [{ type: "text", text: `Deployments to environment ${environmentId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1079
|
-
}
|
|
1080
|
-
catch (error) {
|
|
1081
|
-
console.error("Error getting environment deployments:", error);
|
|
1082
|
-
return { content: [{ type: "text", text: `Failed to get environment deployments: ${error.message}` }] };
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
adminToolsCount++;
|
|
1086
|
-
server.tool("get-environment-checks", "Get all checks (approvals, business hours, branch control, etc.) configured for an environment. Essential for understanding pipeline approval requirements.", {
|
|
1087
|
-
project: z.string().describe("The project name"),
|
|
1088
|
-
environmentId: z.number().describe("The environment ID"),
|
|
1089
|
-
}, async ({ project, environmentId }) => {
|
|
1090
|
-
try {
|
|
1091
|
-
const service = getAzureDevOpsService();
|
|
1092
|
-
const result = await service.getEnvironmentChecks(project, environmentId);
|
|
1093
|
-
return { content: [{ type: "text", text: `Checks for environment ${environmentId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1094
|
-
}
|
|
1095
|
-
catch (error) {
|
|
1096
|
-
console.error("Error getting environment checks:", error);
|
|
1097
|
-
return { content: [{ type: "text", text: `Failed to get environment checks: ${error.message}` }] };
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
adminToolsCount++;
|
|
1101
|
-
}
|
|
1102
|
-
// ========================================
|
|
1103
|
-
// PIPELINE UPSERT TOOLS (Tier 2)
|
|
894
|
+
// PULL REQUEST WRITE TOOLS (Conditional)
|
|
1104
895
|
// ========================================
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
name: z.string().describe("Pipeline name"),
|
|
1109
|
-
repositoryId: z.string().describe("Repository ID (GUID from get-repos or Azure DevOps UI)"),
|
|
1110
|
-
yamlPath: z.string().describe("Path to YAML file in repository (e.g., 'azure-pipelines.yml' or 'pipelines/build.yml')"),
|
|
1111
|
-
folder: z.string().optional().describe("Optional folder path (default: root)"),
|
|
1112
|
-
}, async ({ project, name, repositoryId, yamlPath, folder }) => {
|
|
1113
|
-
try {
|
|
1114
|
-
const service = getAzureDevOpsService();
|
|
1115
|
-
const result = await service.createPipelineDefinition(project, name, repositoryId, yamlPath, folder);
|
|
1116
|
-
return { content: [{ type: "text", text: `Created pipeline '${name}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1117
|
-
}
|
|
1118
|
-
catch (error) {
|
|
1119
|
-
console.error("Error creating pipeline:", error);
|
|
1120
|
-
return { content: [{ type: "text", text: `Failed to create pipeline: ${error.message}` }] };
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
adminToolsCount++;
|
|
1124
|
-
server.tool("admin-update-pipeline", "Update a pipeline definition (name, path, queue status, triggers, or variables). (requires AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true)", {
|
|
1125
|
-
project: z.string().describe("The project name"),
|
|
1126
|
-
definitionId: z.number().describe("The pipeline definition ID"),
|
|
1127
|
-
name: z.string().optional().describe("New pipeline name"),
|
|
1128
|
-
path: z.string().optional().describe("New folder path"),
|
|
1129
|
-
queueStatus: z.enum(["enabled", "disabled", "paused"]).optional().describe("Queue status"),
|
|
1130
|
-
variables: z.record(z.object({
|
|
1131
|
-
value: z.string(),
|
|
1132
|
-
isSecret: z.boolean().optional(),
|
|
1133
|
-
allowOverride: z.boolean().optional()
|
|
1134
|
-
})).optional().describe("Pipeline variables to set"),
|
|
1135
|
-
}, async ({ project, definitionId, name, path, queueStatus, variables }) => {
|
|
1136
|
-
try {
|
|
1137
|
-
const service = getAzureDevOpsService();
|
|
1138
|
-
const updates = {};
|
|
1139
|
-
if (name)
|
|
1140
|
-
updates.name = name;
|
|
1141
|
-
if (path)
|
|
1142
|
-
updates.path = path;
|
|
1143
|
-
if (queueStatus)
|
|
1144
|
-
updates.queueStatus = queueStatus;
|
|
1145
|
-
if (variables)
|
|
1146
|
-
updates.variables = variables;
|
|
1147
|
-
const result = await service.updatePipelineDefinition(project, definitionId, updates);
|
|
1148
|
-
return { content: [{ type: "text", text: `Updated pipeline ${definitionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1149
|
-
}
|
|
1150
|
-
catch (error) {
|
|
1151
|
-
console.error("Error updating pipeline:", error);
|
|
1152
|
-
return { content: [{ type: "text", text: `Failed to update pipeline: ${error.message}` }] };
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
adminToolsCount++;
|
|
1156
|
-
server.tool("admin-rename-pipeline", "Rename a pipeline definition. (requires AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true)", {
|
|
1157
|
-
project: z.string().describe("The project name"),
|
|
1158
|
-
definitionId: z.number().describe("The pipeline definition ID"),
|
|
1159
|
-
newName: z.string().describe("The new pipeline name"),
|
|
1160
|
-
}, async ({ project, definitionId, newName }) => {
|
|
1161
|
-
try {
|
|
1162
|
-
const service = getAzureDevOpsService();
|
|
1163
|
-
const result = await service.renamePipelineDefinition(project, definitionId, newName);
|
|
1164
|
-
return { content: [{ type: "text", text: `Renamed pipeline ${definitionId} to '${newName}':\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1165
|
-
}
|
|
1166
|
-
catch (error) {
|
|
1167
|
-
console.error("Error renaming pipeline:", error);
|
|
1168
|
-
return { content: [{ type: "text", text: `Failed to rename pipeline: ${error.message}` }] };
|
|
1169
|
-
}
|
|
1170
|
-
});
|
|
1171
|
-
adminToolsCount++;
|
|
1172
|
-
server.tool("admin-queue-build", "Queue a new pipeline build/run. Optionally specify branch, variables, or template parameters. (requires AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true)", {
|
|
896
|
+
const enablePullRequestWrite = process.env.AZUREDEVOPS_ENABLE_PR_WRITE === "true";
|
|
897
|
+
if (enablePullRequestWrite) {
|
|
898
|
+
server.tool("add-pull-request-thread", "Add a comment or code review feedback to a pull request. Supports both general comments and inline comments on specific files/lines. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1173
899
|
project: z.string().describe("The project name"),
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
900
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
901
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
902
|
+
content: z.string().describe("Comment content (markdown supported)"),
|
|
903
|
+
filePath: z.string().optional().describe("File path for inline comment (e.g., '/src/file.ts')"),
|
|
904
|
+
lineNumber: z.number().optional().describe("Line number for inline comment (right side of diff)"),
|
|
905
|
+
status: z.enum(["active", "fixed", "wontFix", "closed", "byDesign", "pending"]).optional()
|
|
906
|
+
.describe("Thread status (default: active)"),
|
|
907
|
+
}, async ({ project, repositoryId, pullRequestId, content, filePath, lineNumber, status }) => {
|
|
1179
908
|
try {
|
|
1180
909
|
const service = getAzureDevOpsService();
|
|
1181
|
-
const result = await service.
|
|
1182
|
-
return { content: [{ type: "text", text: `
|
|
910
|
+
const result = await service.addPullRequestThread(project, repositoryId, pullRequestId, content, filePath, lineNumber, status || 'active');
|
|
911
|
+
return { content: [{ type: "text", text: `Added comment to PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1183
912
|
}
|
|
1184
913
|
catch (error) {
|
|
1185
|
-
console.error("Error
|
|
1186
|
-
return { content: [{ type: "text", text: `Failed to
|
|
914
|
+
console.error("Error adding pull request thread:", error);
|
|
915
|
+
return { content: [{ type: "text", text: `Failed to add pull request thread: ${error.message}` }] };
|
|
1187
916
|
}
|
|
1188
917
|
});
|
|
1189
|
-
|
|
1190
|
-
server.tool("admin-cancel-build", "Cancel a running build. The build will finish its current task before stopping. (requires AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true)", {
|
|
918
|
+
server.tool("create-pull-request", "Create a new pull request in a Git repository. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1191
919
|
project: z.string().describe("The project name"),
|
|
1192
|
-
|
|
1193
|
-
|
|
920
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
921
|
+
sourceRefName: z.string().describe(descWithExamples("Source branch full ref name", PR_BRANCH_REF_EXAMPLES)),
|
|
922
|
+
targetRefName: z.string().describe(descWithExamples("Target branch full ref name", PR_BRANCH_REF_EXAMPLES)),
|
|
923
|
+
title: z.string().describe("Pull request title"),
|
|
924
|
+
description: z.string().optional().describe("Pull request description (markdown supported)"),
|
|
925
|
+
reviewerIds: z.array(z.string()).optional().describe("Reviewer GUIDs or unique names"),
|
|
926
|
+
isDraft: z.boolean().optional().describe("Create as draft PR (default: false)"),
|
|
927
|
+
}, async ({ project, repositoryId, sourceRefName, targetRefName, title, description, reviewerIds, isDraft }) => {
|
|
1194
928
|
try {
|
|
1195
929
|
const service = getAzureDevOpsService();
|
|
1196
|
-
const result = await service.
|
|
1197
|
-
return { content: [{ type: "text", text: `
|
|
930
|
+
const result = await service.createPullRequest(project, repositoryId, sourceRefName, targetRefName, title, description, reviewerIds, isDraft);
|
|
931
|
+
return { content: [{ type: "text", text: `Created PR #${result.pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1198
932
|
}
|
|
1199
933
|
catch (error) {
|
|
1200
|
-
console.error("Error
|
|
1201
|
-
return { content: [{ type: "text", text: `Failed to
|
|
934
|
+
console.error("Error creating pull request:", error);
|
|
935
|
+
return { content: [{ type: "text", text: `Failed to create pull request: ${error.message}` }] };
|
|
1202
936
|
}
|
|
1203
937
|
});
|
|
1204
|
-
|
|
1205
|
-
server.tool("admin-retry-build", "Retry a failed build with the same configuration. Creates a new build with same definition, branch, and parameters. (requires AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true)", {
|
|
938
|
+
server.tool("update-pull-request", "Update a pull request's title, description, status, or draft state. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1206
939
|
project: z.string().describe("The project name"),
|
|
1207
|
-
|
|
1208
|
-
|
|
940
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
941
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
942
|
+
title: z.string().optional().describe("New title"),
|
|
943
|
+
description: z.string().optional().describe("New description"),
|
|
944
|
+
status: z.enum(["abandoned", "active"]).optional().describe("Set PR status (abandoned or active)"),
|
|
945
|
+
isDraft: z.boolean().optional().describe("Set draft state"),
|
|
946
|
+
}, async ({ project, repositoryId, pullRequestId, title, description, status, isDraft }) => {
|
|
1209
947
|
try {
|
|
1210
948
|
const service = getAzureDevOpsService();
|
|
1211
|
-
const result = await service.
|
|
1212
|
-
return { content: [{ type: "text", text: `
|
|
949
|
+
const result = await service.updatePullRequest(project, repositoryId, pullRequestId, { title, description, status, isDraft });
|
|
950
|
+
return { content: [{ type: "text", text: `Updated PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1213
951
|
}
|
|
1214
952
|
catch (error) {
|
|
1215
|
-
console.error("Error
|
|
1216
|
-
return { content: [{ type: "text", text: `Failed to
|
|
953
|
+
console.error("Error updating pull request:", error);
|
|
954
|
+
return { content: [{ type: "text", text: `Failed to update pull request: ${error.message}` }] };
|
|
1217
955
|
}
|
|
1218
956
|
});
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
// ========================================
|
|
1222
|
-
// PIPELINE DELETE TOOLS (Tier 3)
|
|
1223
|
-
// ========================================
|
|
1224
|
-
if (enablePipelineDelete) {
|
|
1225
|
-
server.tool("admin-delete-pipeline", "⚠️ DESTRUCTIVE: Delete a pipeline definition. This cannot be undone. (requires AZUREDEVOPS_ENABLE_PIPELINE_DELETE=true)", {
|
|
957
|
+
server.tool("complete-pull-request", "Complete (merge) a pull request with configurable merge strategy. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1226
958
|
project: z.string().describe("The project name"),
|
|
1227
|
-
|
|
1228
|
-
|
|
959
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
960
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
961
|
+
mergeStrategy: z.enum(["squash", "noFastForward", "rebase", "rebaseMerge"]).optional()
|
|
962
|
+
.describe(descWithExamples("Merge strategy (default: squash)", PR_MERGE_STRATEGY_EXAMPLES)),
|
|
963
|
+
deleteSourceBranch: z.boolean().optional().describe("Delete source branch after merge (default: true)"),
|
|
964
|
+
transitionWorkItems: z.boolean().optional().describe("Transition linked work items (default: true)"),
|
|
965
|
+
mergeCommitMessage: z.string().optional().describe("Custom merge commit message"),
|
|
966
|
+
}, async ({ project, repositoryId, pullRequestId, mergeStrategy, deleteSourceBranch, transitionWorkItems, mergeCommitMessage }) => {
|
|
1229
967
|
try {
|
|
1230
968
|
const service = getAzureDevOpsService();
|
|
1231
|
-
const result = await service.
|
|
1232
|
-
return { content: [{ type: "text", text: `
|
|
969
|
+
const result = await service.completePullRequest(project, repositoryId, pullRequestId, mergeStrategy || 'squash', deleteSourceBranch !== undefined ? deleteSourceBranch : true, transitionWorkItems !== undefined ? transitionWorkItems : true, mergeCommitMessage);
|
|
970
|
+
return { content: [{ type: "text", text: `Completed PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1233
971
|
}
|
|
1234
972
|
catch (error) {
|
|
1235
|
-
console.error("Error
|
|
1236
|
-
return { content: [{ type: "text", text: `Failed to
|
|
973
|
+
console.error("Error completing pull request:", error);
|
|
974
|
+
return { content: [{ type: "text", text: `Failed to complete pull request: ${error.message}` }] };
|
|
1237
975
|
}
|
|
1238
976
|
});
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
// ========================================
|
|
1242
|
-
// SERVICE CONNECTION UPSERT TOOLS (Tier 2)
|
|
1243
|
-
// ========================================
|
|
1244
|
-
if (enableServiceConnUpsert) {
|
|
1245
|
-
server.tool("admin-create-service-connection", "Create a new service connection. Use get-service-connection-types to see available types and auth schemes. (requires AZUREDEVOPS_ENABLE_SERVICE_CONN_UPSERT=true)", {
|
|
977
|
+
server.tool("add-pr-reviewer", "Add or remove a reviewer from a pull request. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1246
978
|
project: z.string().describe("The project name"),
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
parameters: z.record(z.string()).optional().describe("Auth parameters (credentials)")
|
|
1254
|
-
}).optional().describe("Authorization configuration"),
|
|
1255
|
-
data: z.record(z.string()).optional().describe("Type-specific configuration data"),
|
|
1256
|
-
}, async ({ project, name, type, url, description, authorization, data }) => {
|
|
979
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
980
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
981
|
+
reviewerId: z.string().describe("Reviewer GUID or unique name"),
|
|
982
|
+
isRequired: z.boolean().optional().describe("Whether the reviewer is required (default: false)"),
|
|
983
|
+
remove: z.boolean().optional().describe("Set to true to remove the reviewer instead of adding"),
|
|
984
|
+
}, async ({ project, repositoryId, pullRequestId, reviewerId, isRequired, remove }) => {
|
|
1257
985
|
try {
|
|
1258
986
|
const service = getAzureDevOpsService();
|
|
1259
|
-
const result = await service.
|
|
1260
|
-
return { content: [{ type: "text", text:
|
|
987
|
+
const result = await service.addOrRemovePrReviewer(project, repositoryId, pullRequestId, reviewerId, isRequired, remove);
|
|
988
|
+
return { content: [{ type: "text", text: `${remove ? 'Removed' : 'Added'} reviewer on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1261
989
|
}
|
|
1262
990
|
catch (error) {
|
|
1263
|
-
console.error("Error
|
|
1264
|
-
return { content: [{ type: "text", text: `Failed to
|
|
991
|
+
console.error("Error managing PR reviewer:", error);
|
|
992
|
+
return { content: [{ type: "text", text: `Failed to manage PR reviewer: ${error.message}` }] };
|
|
1265
993
|
}
|
|
1266
994
|
});
|
|
1267
|
-
|
|
1268
|
-
server.tool("admin-update-service-connection", "Update a service connection's metadata (name, description, URL, data). Cannot update credentials for security. (requires AZUREDEVOPS_ENABLE_SERVICE_CONN_UPSERT=true)", {
|
|
995
|
+
server.tool("vote-pull-request", "Submit a vote (approve, reject, etc.) on a pull request. Defaults to authenticated user. (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1269
996
|
project: z.string().describe("The project name"),
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}, async ({ project,
|
|
1276
|
-
try {
|
|
1277
|
-
const service = getAzureDevOpsService();
|
|
1278
|
-
const updates = {};
|
|
1279
|
-
if (name)
|
|
1280
|
-
updates.name = name;
|
|
1281
|
-
if (description)
|
|
1282
|
-
updates.description = description;
|
|
1283
|
-
if (url)
|
|
1284
|
-
updates.url = url;
|
|
1285
|
-
if (data)
|
|
1286
|
-
updates.data = data;
|
|
1287
|
-
const result = await service.updateServiceConnection(project, connectionId, updates);
|
|
1288
|
-
return { content: [{ type: "text", text: `Updated service connection ${connectionId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1289
|
-
}
|
|
1290
|
-
catch (error) {
|
|
1291
|
-
console.error("Error updating service connection:", error);
|
|
1292
|
-
return { content: [{ type: "text", text: `Failed to update service connection: ${error.message}` }] };
|
|
1293
|
-
}
|
|
1294
|
-
});
|
|
1295
|
-
adminToolsCount++;
|
|
1296
|
-
server.tool("admin-share-service-connection", "Share a service connection with other projects. (requires AZUREDEVOPS_ENABLE_SERVICE_CONN_UPSERT=true)", {
|
|
1297
|
-
connectionId: z.string().describe("The service connection ID (GUID)"),
|
|
1298
|
-
projectIds: z.array(z.string()).describe("Array of project IDs to share with"),
|
|
1299
|
-
}, async ({ connectionId, projectIds }) => {
|
|
997
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
998
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
999
|
+
vote: z.enum(["approve", "approveWithSuggestions", "noResponse", "waitForAuthor", "reject"])
|
|
1000
|
+
.describe(descWithExamples("Vote to submit", PR_VOTE_EXAMPLES)),
|
|
1001
|
+
reviewerId: z.string().optional().describe("Reviewer GUID (defaults to authenticated user)"),
|
|
1002
|
+
}, async ({ project, repositoryId, pullRequestId, vote, reviewerId }) => {
|
|
1300
1003
|
try {
|
|
1301
1004
|
const service = getAzureDevOpsService();
|
|
1302
|
-
const result = await service.
|
|
1303
|
-
return { content: [{ type: "text", text: `
|
|
1005
|
+
const result = await service.votePullRequest(project, repositoryId, pullRequestId, vote, reviewerId);
|
|
1006
|
+
return { content: [{ type: "text", text: `Voted '${vote}' on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1304
1007
|
}
|
|
1305
1008
|
catch (error) {
|
|
1306
|
-
console.error("Error
|
|
1307
|
-
return { content: [{ type: "text", text: `Failed to
|
|
1009
|
+
console.error("Error voting on pull request:", error);
|
|
1010
|
+
return { content: [{ type: "text", text: `Failed to vote on pull request: ${error.message}` }] };
|
|
1308
1011
|
}
|
|
1309
1012
|
});
|
|
1310
|
-
|
|
1311
|
-
}
|
|
1312
|
-
// ========================================
|
|
1313
|
-
// SERVICE CONNECTION DELETE TOOLS (Tier 3)
|
|
1314
|
-
// ========================================
|
|
1315
|
-
if (enableServiceConnDelete) {
|
|
1316
|
-
server.tool("admin-delete-service-connection", "⚠️ DESTRUCTIVE: Delete a service connection. Pipelines using this connection will fail. (requires AZUREDEVOPS_ENABLE_SERVICE_CONN_DELETE=true)", {
|
|
1013
|
+
server.tool("reply-to-pr-thread", "Reply to a pull request comment thread and/or update thread status (e.g., resolve). (requires AZUREDEVOPS_ENABLE_PR_WRITE=true)", {
|
|
1317
1014
|
project: z.string().describe("The project name"),
|
|
1318
|
-
|
|
1319
|
-
|
|
1015
|
+
repositoryId: z.string().describe("Repository ID (GUID) or name"),
|
|
1016
|
+
pullRequestId: z.number().describe("The pull request ID"),
|
|
1017
|
+
threadId: z.number().describe("The thread ID to reply to"),
|
|
1018
|
+
content: z.string().optional().describe("Reply text (markdown supported)"),
|
|
1019
|
+
status: z.enum(["active", "fixed", "wontFix", "closed", "byDesign", "pending"]).optional()
|
|
1020
|
+
.describe("Update thread status (e.g., 'fixed' to resolve)"),
|
|
1021
|
+
}, async ({ project, repositoryId, pullRequestId, threadId, content, status }) => {
|
|
1320
1022
|
try {
|
|
1321
1023
|
const service = getAzureDevOpsService();
|
|
1322
|
-
const result = await service.
|
|
1323
|
-
return { content: [{ type: "text", text: `
|
|
1024
|
+
const result = await service.replyToPrThread(project, repositoryId, pullRequestId, threadId, content, status);
|
|
1025
|
+
return { content: [{ type: "text", text: `Reply to thread #${threadId} on PR #${pullRequestId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1324
1026
|
}
|
|
1325
1027
|
catch (error) {
|
|
1326
|
-
console.error("Error
|
|
1327
|
-
return { content: [{ type: "text", text: `Failed to
|
|
1028
|
+
console.error("Error replying to PR thread:", error);
|
|
1029
|
+
return { content: [{ type: "text", text: `Failed to reply to PR thread: ${error.message}` }] };
|
|
1328
1030
|
}
|
|
1329
1031
|
});
|
|
1330
|
-
adminToolsCount++;
|
|
1331
1032
|
}
|
|
1332
1033
|
// ========================================
|
|
1333
|
-
// VARIABLE GROUP
|
|
1034
|
+
// VARIABLE GROUP TOOLS
|
|
1334
1035
|
// ========================================
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
return { content: [{ type: "text", text: `Failed to set variable: ${error.message}` }] };
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
adminToolsCount++;
|
|
1396
|
-
}
|
|
1397
|
-
// ========================================
|
|
1398
|
-
// VARIABLE GROUP DELETE TOOLS (Tier 3)
|
|
1399
|
-
// ========================================
|
|
1400
|
-
if (enableVariableGroupDelete) {
|
|
1401
|
-
server.tool("admin-remove-variable", "Remove a variable from a variable group. (requires AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE=true)", {
|
|
1402
|
-
project: z.string().describe("The project name"),
|
|
1403
|
-
groupId: z.number().describe("The variable group ID"),
|
|
1404
|
-
variableName: z.string().describe("Variable name to remove"),
|
|
1405
|
-
}, async ({ project, groupId, variableName }) => {
|
|
1406
|
-
try {
|
|
1407
|
-
const service = getAzureDevOpsService();
|
|
1408
|
-
const result = await service.removeVariable(project, groupId, variableName);
|
|
1409
|
-
return { content: [{ type: "text", text: `Removed variable '${variableName}' from group ${groupId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1410
|
-
}
|
|
1411
|
-
catch (error) {
|
|
1412
|
-
console.error("Error removing variable:", error);
|
|
1413
|
-
return { content: [{ type: "text", text: `Failed to remove variable: ${error.message}` }] };
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
adminToolsCount++;
|
|
1417
|
-
server.tool("admin-delete-variable-group", "⚠️ DESTRUCTIVE: Delete a variable group. Pipelines using this group will fail. (requires AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE=true)", {
|
|
1418
|
-
project: z.string().describe("The project name"),
|
|
1419
|
-
groupId: z.number().describe("The variable group ID to delete"),
|
|
1420
|
-
}, async ({ project, groupId }) => {
|
|
1421
|
-
try {
|
|
1422
|
-
const service = getAzureDevOpsService();
|
|
1423
|
-
const result = await service.deleteVariableGroup(project, groupId);
|
|
1424
|
-
return { content: [{ type: "text", text: `Deleted variable group ${groupId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1425
|
-
}
|
|
1426
|
-
catch (error) {
|
|
1427
|
-
console.error("Error deleting variable group:", error);
|
|
1428
|
-
return { content: [{ type: "text", text: `Failed to delete variable group: ${error.message}` }] };
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
adminToolsCount++;
|
|
1432
|
-
}
|
|
1433
|
-
// ========================================
|
|
1434
|
-
// AGENT POOL UPSERT TOOLS (Tier 2)
|
|
1435
|
-
// ========================================
|
|
1436
|
-
if (enableAgentPoolUpsert) {
|
|
1437
|
-
server.tool("admin-update-agent-pool", "Update agent pool settings (auto-provision, auto-update, auto-size, target size). (requires AZUREDEVOPS_ENABLE_AGENT_POOL_UPSERT=true)", {
|
|
1438
|
-
poolId: z.number().describe("The agent pool ID"),
|
|
1439
|
-
autoProvision: z.boolean().optional().describe("Auto-provision pool to new projects"),
|
|
1440
|
-
autoUpdate: z.boolean().optional().describe("Auto-update agents"),
|
|
1441
|
-
autoSize: z.boolean().optional().describe("Auto-size pool based on demand"),
|
|
1442
|
-
targetSize: z.number().optional().describe("Target pool size for auto-scaling"),
|
|
1443
|
-
}, async ({ poolId, autoProvision, autoUpdate, autoSize, targetSize }) => {
|
|
1444
|
-
try {
|
|
1445
|
-
const service = getAzureDevOpsService();
|
|
1446
|
-
const updates = {};
|
|
1447
|
-
if (autoProvision !== undefined)
|
|
1448
|
-
updates.autoProvision = autoProvision;
|
|
1449
|
-
if (autoUpdate !== undefined)
|
|
1450
|
-
updates.autoUpdate = autoUpdate;
|
|
1451
|
-
if (autoSize !== undefined)
|
|
1452
|
-
updates.autoSize = autoSize;
|
|
1453
|
-
if (targetSize !== undefined)
|
|
1454
|
-
updates.targetSize = targetSize;
|
|
1455
|
-
const result = await service.updateAgentPool(poolId, updates);
|
|
1456
|
-
return { content: [{ type: "text", text: `Updated agent pool ${poolId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1457
|
-
}
|
|
1458
|
-
catch (error) {
|
|
1459
|
-
console.error("Error updating agent pool:", error);
|
|
1460
|
-
return { content: [{ type: "text", text: `Failed to update agent pool: ${error.message}` }] };
|
|
1461
|
-
}
|
|
1462
|
-
});
|
|
1463
|
-
adminToolsCount++;
|
|
1464
|
-
server.tool("admin-enable-agent", "Enable a disabled agent to accept new jobs. (requires AZUREDEVOPS_ENABLE_AGENT_POOL_UPSERT=true)", {
|
|
1465
|
-
poolId: z.number().describe("The agent pool ID"),
|
|
1466
|
-
agentId: z.number().describe("The agent ID"),
|
|
1467
|
-
}, async ({ poolId, agentId }) => {
|
|
1468
|
-
try {
|
|
1469
|
-
const service = getAzureDevOpsService();
|
|
1470
|
-
const result = await service.enableAgent(poolId, agentId);
|
|
1471
|
-
return { content: [{ type: "text", text: `Enabled agent ${agentId} in pool ${poolId}:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1472
|
-
}
|
|
1473
|
-
catch (error) {
|
|
1474
|
-
console.error("Error enabling agent:", error);
|
|
1475
|
-
return { content: [{ type: "text", text: `Failed to enable agent: ${error.message}` }] };
|
|
1476
|
-
}
|
|
1477
|
-
});
|
|
1478
|
-
adminToolsCount++;
|
|
1479
|
-
}
|
|
1036
|
+
server.tool("list-variable-groups", "List all variable groups in an Azure DevOps project. Variable groups store values and secrets used in pipelines.", {
|
|
1037
|
+
project: z.string().describe("The project name"),
|
|
1038
|
+
}, async ({ project }) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const service = getAzureDevOpsService();
|
|
1041
|
+
const result = await service.getVariableGroups(project);
|
|
1042
|
+
const resultStr = JSON.stringify(result, null, 2);
|
|
1043
|
+
return {
|
|
1044
|
+
content: [
|
|
1045
|
+
{
|
|
1046
|
+
type: "text",
|
|
1047
|
+
text: `Variable groups in project '${project}':\n\n${resultStr}`,
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
console.error("Error listing variable groups:", error);
|
|
1054
|
+
return {
|
|
1055
|
+
content: [
|
|
1056
|
+
{
|
|
1057
|
+
type: "text",
|
|
1058
|
+
text: `Failed to list variable groups: ${error.message}`,
|
|
1059
|
+
},
|
|
1060
|
+
],
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
server.tool("get-variable-group", "Get a specific variable group by ID from Azure DevOps. Returns all variables (secrets are masked).", {
|
|
1065
|
+
project: z.string().describe("The project name"),
|
|
1066
|
+
groupId: z.number().describe("The variable group ID"),
|
|
1067
|
+
}, async ({ project, groupId }) => {
|
|
1068
|
+
try {
|
|
1069
|
+
const service = getAzureDevOpsService();
|
|
1070
|
+
const result = await service.getVariableGroup(project, groupId);
|
|
1071
|
+
const resultStr = JSON.stringify(result, null, 2);
|
|
1072
|
+
return {
|
|
1073
|
+
content: [
|
|
1074
|
+
{
|
|
1075
|
+
type: "text",
|
|
1076
|
+
text: `Variable group ${groupId} in project '${project}':\n\n${resultStr}`,
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
console.error("Error getting variable group:", error);
|
|
1083
|
+
return {
|
|
1084
|
+
content: [
|
|
1085
|
+
{
|
|
1086
|
+
type: "text",
|
|
1087
|
+
text: `Failed to get variable group: ${error.message}`,
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1480
1093
|
// ========================================
|
|
1481
|
-
//
|
|
1094
|
+
// BUILD TROUBLESHOOTING TOOLS (Read-only)
|
|
1095
|
+
// NOTE: These tools are duplicated in azure-devops-admin package.
|
|
1096
|
+
// If you update these, also update packages/azure-devops-admin/src/index.ts
|
|
1482
1097
|
// ========================================
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1098
|
+
server.tool("get-build-status", "Get build status and details. Use detail='summary' for basic status, 'timeline' for step breakdown (default scope='problems' shows only failed/warning items), or 'full' for logs. The timelineScope controls what records are included: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything).", {
|
|
1099
|
+
project: z.string().describe("The project name"),
|
|
1100
|
+
buildId: z.number().describe("The build ID"),
|
|
1101
|
+
detail: z.enum(["summary", "timeline", "full"]).optional().describe("Level of detail: 'summary' (default), 'timeline' (include steps), or 'full' (include logs)"),
|
|
1102
|
+
timelineScope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Timeline scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
1103
|
+
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
1104
|
+
}, async ({ project, buildId, detail, timelineScope, maxIssues }) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const service = getAzureDevOpsService();
|
|
1107
|
+
const result = await service.getBuildStatus(project, buildId, detail || 'summary', timelineScope || 'problems', maxIssues || 5);
|
|
1108
|
+
return { content: [{ type: "text", text: `Build ${buildId} status:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
console.error("Error getting build status:", error);
|
|
1112
|
+
return { content: [{ type: "text", text: `Failed to get build status: ${error.message}` }] };
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
server.tool("get-build-timeline", "Get step-by-step breakdown of a build. Shows stages, jobs, and tasks with timing, status, and error/warning counts. Use scope to control output size: 'problems' (default, only errors/warnings), 'stages' (minimal), 'jobs' (moderate), 'all' (everything). Always includes summary stats regardless of scope.", {
|
|
1116
|
+
project: z.string().describe("The project name"),
|
|
1117
|
+
buildId: z.number().describe("The build ID"),
|
|
1118
|
+
scope: z.enum(["stages", "jobs", "all", "problems"]).optional().describe("Filter scope: 'problems' (default, only errors/warnings/failures), 'stages' (minimal), 'jobs' (moderate), 'all' (everything - may be large)"),
|
|
1119
|
+
maxIssues: z.number().optional().describe("Maximum issues per record (default: 5, prioritizes errors over warnings)"),
|
|
1120
|
+
}, async ({ project, buildId, scope, maxIssues }) => {
|
|
1121
|
+
try {
|
|
1122
|
+
const service = getAzureDevOpsService();
|
|
1123
|
+
const result = await service.getBuildTimeline(project, buildId, scope || 'problems', maxIssues || 5);
|
|
1124
|
+
return { content: [{ type: "text", text: `Build ${buildId} timeline:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
console.error("Error getting build timeline:", error);
|
|
1128
|
+
return { content: [{ type: "text", text: `Failed to get build timeline: ${error.message}` }] };
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
server.tool("get-build-logs", "Get build logs. Without logId, returns list of available logs with line counts. With logId, returns that log's content. NOTE: For verbose logs, consider using a sub-agent to analyze the content.", {
|
|
1132
|
+
project: z.string().describe("The project name"),
|
|
1133
|
+
buildId: z.number().describe("The build ID"),
|
|
1134
|
+
logId: z.number().optional().describe("Optional specific log ID to retrieve content"),
|
|
1135
|
+
}, async ({ project, buildId, logId }) => {
|
|
1136
|
+
try {
|
|
1137
|
+
const service = getAzureDevOpsService();
|
|
1138
|
+
const result = await service.getBuildLogs(project, buildId, logId);
|
|
1139
|
+
return { content: [{ type: "text", text: `Build ${buildId} logs:\n\n${JSON.stringify(result, null, 2)}` }] };
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
console.error("Error getting build logs:", error);
|
|
1143
|
+
return { content: [{ type: "text", text: `Failed to get build logs: ${error.message}` }] };
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1500
1146
|
// ========================================
|
|
1501
|
-
//
|
|
1147
|
+
// WORK ITEM SYNC TOOLS (Local Markdown)
|
|
1502
1148
|
// ========================================
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1149
|
+
server.tool("sync-work-item-to-file", descWithExamples("Download work item(s) from ADO and save as local markdown file(s). Token-efficient for editing. Auto-converts HTML fields to markdown. Can also pull all child work items under a parent (e.g., all User Stories under a Feature).", SYNC_TO_FILE_EXAMPLES), {
|
|
1150
|
+
project: z.string().describe("The project name"),
|
|
1151
|
+
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to pull (optional if using parentId)"),
|
|
1152
|
+
parentId: z.number().optional().describe("Pull all child work items of this parent (e.g., Feature ID to pull all User Stories)"),
|
|
1153
|
+
childType: z.string().optional().describe("Filter by work item type when using parentId (default: 'User Story'). Common values: 'User Story', 'Bug', 'Task'"),
|
|
1154
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1155
|
+
includeComments: z.boolean().optional().describe("Also save comments to {id}-comments.md (default: false)"),
|
|
1156
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1157
|
+
}, async ({ project, workItemIds: providedWorkItemIds, parentId, childType, folder, includeComments, skipAutoConvert }) => {
|
|
1158
|
+
try {
|
|
1159
|
+
const service = getAzureDevOpsService();
|
|
1160
|
+
const syncConfig = getSyncConfig(folder);
|
|
1161
|
+
// Validate folder path for security
|
|
1162
|
+
validateFolderPath(syncConfig.folder);
|
|
1163
|
+
// Determine which work items to pull
|
|
1164
|
+
let workItemIds = providedWorkItemIds || [];
|
|
1165
|
+
// If parentId is provided, query for child work items
|
|
1166
|
+
if (parentId) {
|
|
1167
|
+
const type = childType || 'User Story';
|
|
1168
|
+
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = '${type}' ORDER BY [System.Id] ASC`;
|
|
1169
|
+
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1170
|
+
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1171
|
+
const childIds = queryResult.workItems.map((wi) => wi.id);
|
|
1172
|
+
workItemIds = [...workItemIds, ...childIds];
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// Validate that we have work items to pull
|
|
1176
|
+
if (workItemIds.length === 0) {
|
|
1177
|
+
return {
|
|
1178
|
+
content: [{
|
|
1179
|
+
type: "text",
|
|
1180
|
+
text: parentId
|
|
1181
|
+
? `No ${childType || 'User Story'} work items found under parent #${parentId}`
|
|
1182
|
+
: "No work item IDs provided. Specify workItemIds or parentId.",
|
|
1183
|
+
}],
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
// Remove duplicates
|
|
1187
|
+
workItemIds = [...new Set(workItemIds)];
|
|
1188
|
+
const pulled = [];
|
|
1189
|
+
const skipped = [];
|
|
1190
|
+
const commentsFiles = [];
|
|
1191
|
+
const filesToCommit = [];
|
|
1192
|
+
await ensureFolderExists(syncConfig.folder);
|
|
1193
|
+
for (const workItemId of workItemIds) {
|
|
1194
|
+
try {
|
|
1195
|
+
// Fetch work item from ADO
|
|
1196
|
+
let workItem = await service.getWorkItem(project, workItemId);
|
|
1197
|
+
let revision = workItem.rev || workItem._rev || 1;
|
|
1198
|
+
let convertedFields = [];
|
|
1199
|
+
// Check field formats
|
|
1200
|
+
const formats = checkFieldFormats(workItem);
|
|
1201
|
+
if (!formats.ready) {
|
|
1202
|
+
if (skipAutoConvert) {
|
|
1203
|
+
// User explicitly requested to skip conversion
|
|
1204
|
+
const htmlFields = [];
|
|
1205
|
+
if (formats.description === 'html')
|
|
1206
|
+
htmlFields.push('Description');
|
|
1207
|
+
if (formats.acceptanceCriteria === 'html')
|
|
1208
|
+
htmlFields.push('Acceptance Criteria');
|
|
1209
|
+
skipped.push({
|
|
1210
|
+
id: workItemId,
|
|
1211
|
+
reason: `HTML fields: ${htmlFields.join(', ')}. skipAutoConvert=true, skipping.`,
|
|
1212
|
+
});
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
// Auto-convert HTML fields to markdown in ADO
|
|
1216
|
+
const fieldsToConvert = [];
|
|
1217
|
+
if (formats.description === 'html')
|
|
1218
|
+
fieldsToConvert.push('System.Description');
|
|
1219
|
+
if (formats.acceptanceCriteria === 'html')
|
|
1220
|
+
fieldsToConvert.push('Microsoft.VSTS.Common.AcceptanceCriteria');
|
|
1221
|
+
try {
|
|
1222
|
+
convertedFields = await autoConvertFieldsToMarkdown(service, project, workItemId, workItem.fields, fieldsToConvert);
|
|
1223
|
+
// Re-fetch work item with converted content
|
|
1224
|
+
workItem = await service.getWorkItem(project, workItemId);
|
|
1225
|
+
revision = workItem.rev || workItem._rev || 1;
|
|
1226
|
+
}
|
|
1227
|
+
catch (convertError) {
|
|
1228
|
+
skipped.push({
|
|
1229
|
+
id: workItemId,
|
|
1230
|
+
reason: `Failed to auto-convert HTML: ${convertError.message}`,
|
|
1231
|
+
});
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// Convert to markdown and save
|
|
1236
|
+
const { content: markdown, skippedFields: secondarySkipped } = workItemToMarkdown(workItem, revision);
|
|
1237
|
+
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
1238
|
+
await writeWorkItemFile(filePath, markdown);
|
|
1239
|
+
pulled.push({
|
|
1240
|
+
id: workItemId,
|
|
1241
|
+
file: filePath,
|
|
1242
|
+
revision,
|
|
1243
|
+
...(convertedFields.length > 0 ? { converted: convertedFields } : {}),
|
|
1244
|
+
...(secondarySkipped.length > 0 ? { skippedFields: secondarySkipped } : {}),
|
|
1245
|
+
});
|
|
1246
|
+
filesToCommit.push({ filePath, workItemId });
|
|
1247
|
+
// Optionally save comments
|
|
1248
|
+
if (includeComments) {
|
|
1249
|
+
const comments = await service.getWorkItemComments(project, workItemId);
|
|
1250
|
+
const commentsMarkdown = commentsToMarkdown(workItem, comments.comments || []);
|
|
1251
|
+
const commentsPath = getCommentsFilePath(syncConfig.folder, workItemId);
|
|
1252
|
+
await writeWorkItemFile(commentsPath, commentsMarkdown);
|
|
1253
|
+
commentsFiles.push({
|
|
1254
|
+
id: workItemId,
|
|
1255
|
+
file: commentsPath,
|
|
1256
|
+
count: (comments.comments || []).length,
|
|
1257
|
+
});
|
|
1258
|
+
filesToCommit.push({ filePath: commentsPath, workItemId });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
skipped.push({ id: workItemId, reason: error.message });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// Auto-commit if enabled
|
|
1266
|
+
let committed = false;
|
|
1267
|
+
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1268
|
+
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced');
|
|
1269
|
+
committed = commitResult.committed;
|
|
1270
|
+
}
|
|
1271
|
+
const result = {
|
|
1272
|
+
pulled,
|
|
1273
|
+
skipped,
|
|
1274
|
+
...(includeComments ? { commentsFiles } : {}),
|
|
1275
|
+
folder: syncConfig.folder,
|
|
1276
|
+
committed,
|
|
1277
|
+
};
|
|
1278
|
+
return {
|
|
1279
|
+
content: [{
|
|
1280
|
+
type: "text",
|
|
1281
|
+
text: `Synced ${pulled.length} work item(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1282
|
+
}],
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
console.error("Error syncing work items to files:", error);
|
|
1287
|
+
return {
|
|
1288
|
+
content: [{
|
|
1289
|
+
type: "text",
|
|
1290
|
+
text: `Failed to sync work items: ${error.message}`,
|
|
1291
|
+
}],
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
server.tool("sync-work-item-from-file", descWithExamples("Upload local markdown changes back to ADO. Auto-detects new_*.md files and creates them as new work items. Auto-converts HTML fields to markdown. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", SYNC_FROM_FILE_EXAMPLES), {
|
|
1296
|
+
project: z.string().describe("The project name"),
|
|
1297
|
+
workItemIds: z.array(z.number()).default([]).describe("Work item IDs to push (optional - new_*.md files are auto-detected)"),
|
|
1298
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1299
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion. Only use when explicitly requested. Default: false (auto-convert enabled)"),
|
|
1300
|
+
}, async ({ project, workItemIds, folder, skipAutoConvert }) => {
|
|
1301
|
+
try {
|
|
1302
|
+
const service = getAzureDevOpsService();
|
|
1303
|
+
const syncConfig = getSyncConfig(folder);
|
|
1304
|
+
// Validate folder path for security
|
|
1305
|
+
validateFolderPath(syncConfig.folder);
|
|
1306
|
+
const pushed = [];
|
|
1307
|
+
const partial = [];
|
|
1308
|
+
const created = [];
|
|
1309
|
+
const failed = [];
|
|
1310
|
+
// Step 1: Auto-detect and process new_*.md files
|
|
1311
|
+
const newFiles = await findNewWorkItemFiles(syncConfig.folder);
|
|
1312
|
+
for (const filePath of newFiles) {
|
|
1313
|
+
try {
|
|
1314
|
+
// Parse the new work item file
|
|
1315
|
+
const content = await readFileContent(filePath);
|
|
1316
|
+
const parsed = parseNewWorkItemMarkdown(content);
|
|
1317
|
+
// Fetch parent work item to inherit area/iteration paths
|
|
1318
|
+
const parentWorkItem = await service.getWorkItem(project, parsed.frontmatter.parent);
|
|
1319
|
+
// Build fields for creation
|
|
1320
|
+
const fields = buildNewWorkItemFields(parsed, parentWorkItem);
|
|
1321
|
+
// Create the work item in ADO with parent link
|
|
1322
|
+
const createdWorkItem = await service.createWorkItem(project, parsed.frontmatter.type || 'User Story', fields, parsed.frontmatter.parent);
|
|
1323
|
+
const newId = createdWorkItem.id;
|
|
1324
|
+
const revision = createdWorkItem.rev || createdWorkItem._rev || 1;
|
|
1325
|
+
const url = createdWorkItem._links?.html?.href || `https://dev.azure.com/_workitems/edit/${newId}`;
|
|
1326
|
+
// Convert the file content to synced format
|
|
1327
|
+
const syncedContent = convertNewFileToSynced(content, newId, revision, url);
|
|
1328
|
+
// Rename file from new_*.md to {id}.md
|
|
1329
|
+
const newFilePath = getWorkItemFilePath(syncConfig.folder, newId);
|
|
1330
|
+
await writeWorkItemFile(newFilePath, syncedContent);
|
|
1331
|
+
await renameFile(filePath, filePath + '.created'); // Mark original as processed
|
|
1332
|
+
// Actually delete the .created file (cleanup)
|
|
1333
|
+
try {
|
|
1334
|
+
const fs = await import('node:fs/promises');
|
|
1335
|
+
await fs.unlink(filePath + '.created');
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
// Ignore cleanup errors
|
|
1339
|
+
}
|
|
1340
|
+
created.push({
|
|
1341
|
+
id: newId,
|
|
1342
|
+
oldFile: filePath,
|
|
1343
|
+
newFile: newFilePath,
|
|
1344
|
+
parentId: parsed.frontmatter.parent,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
failed.push({ file: filePath, error: error.message });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Step 2: Process existing work items (if workItemIds provided)
|
|
1352
|
+
const idsToProcess = workItemIds || [];
|
|
1353
|
+
for (const workItemId of idsToProcess) {
|
|
1354
|
+
const filePath = getWorkItemFilePath(syncConfig.folder, workItemId);
|
|
1355
|
+
try {
|
|
1356
|
+
// Check if file exists
|
|
1357
|
+
if (!await fileExists(filePath)) {
|
|
1358
|
+
failed.push({ id: workItemId, error: `File not found: ${filePath}` });
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
// Parse local file
|
|
1362
|
+
const parsed = await readWorkItemFile(filePath);
|
|
1363
|
+
const oldRevision = parsed.frontmatter.lastSyncedRevision;
|
|
1364
|
+
// Fetch current work item from ADO to compare
|
|
1365
|
+
const currentWorkItem = await service.getWorkItem(project, workItemId);
|
|
1366
|
+
// Build patch operations (auto-converts HTML fields unless skipAutoConvert)
|
|
1367
|
+
const { operations, skippedFields, convertedFields } = buildPatchOperations(parsed, currentWorkItem, skipAutoConvert);
|
|
1368
|
+
if (operations.length === 0 && skippedFields.length === 0) {
|
|
1369
|
+
// No changes
|
|
1370
|
+
pushed.push({
|
|
1371
|
+
id: workItemId,
|
|
1372
|
+
oldRevision,
|
|
1373
|
+
newRevision: currentWorkItem.rev || currentWorkItem._rev || oldRevision,
|
|
1374
|
+
fieldsUpdated: [],
|
|
1375
|
+
});
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
// Update ADO if there are operations
|
|
1379
|
+
let newRevision = oldRevision;
|
|
1380
|
+
let fieldsUpdated = [];
|
|
1381
|
+
if (operations.length > 0) {
|
|
1382
|
+
const updatedWorkItem = await service.updateWorkItem(project, workItemId, operations);
|
|
1383
|
+
newRevision = updatedWorkItem.rev || updatedWorkItem._rev || oldRevision + 1;
|
|
1384
|
+
// Filter out format operations from fieldsUpdated
|
|
1385
|
+
fieldsUpdated = operations
|
|
1386
|
+
.filter(op => op.path.startsWith('/fields/'))
|
|
1387
|
+
.map(op => op.path.replace('/fields/', ''));
|
|
1388
|
+
// Update local file with new revision
|
|
1389
|
+
const content = await readFileContent(filePath);
|
|
1390
|
+
const updatedContent = updateSyncRevision(content, newRevision);
|
|
1391
|
+
await writeWorkItemFile(filePath, updatedContent);
|
|
1392
|
+
}
|
|
1393
|
+
if (skippedFields.length > 0) {
|
|
1394
|
+
partial.push({
|
|
1395
|
+
id: workItemId,
|
|
1396
|
+
oldRevision,
|
|
1397
|
+
newRevision,
|
|
1398
|
+
fieldsUpdated,
|
|
1399
|
+
skippedFields,
|
|
1400
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
pushed.push({
|
|
1405
|
+
id: workItemId,
|
|
1406
|
+
oldRevision,
|
|
1407
|
+
newRevision,
|
|
1408
|
+
fieldsUpdated,
|
|
1409
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
failed.push({ id: workItemId, error: error.message });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const result = {
|
|
1418
|
+
created,
|
|
1419
|
+
pushed,
|
|
1420
|
+
partial,
|
|
1421
|
+
failed,
|
|
1422
|
+
folder: syncConfig.folder,
|
|
1423
|
+
};
|
|
1424
|
+
const summary = [];
|
|
1425
|
+
if (created.length > 0)
|
|
1426
|
+
summary.push(`Created ${created.length} new work item(s)`);
|
|
1427
|
+
if (pushed.length > 0)
|
|
1428
|
+
summary.push(`Updated ${pushed.length} work item(s)`);
|
|
1429
|
+
if (partial.length > 0)
|
|
1430
|
+
summary.push(`Partially updated ${partial.length} work item(s)`);
|
|
1431
|
+
if (failed.length > 0)
|
|
1432
|
+
summary.push(`Failed: ${failed.length}`);
|
|
1433
|
+
return {
|
|
1434
|
+
content: [{
|
|
1435
|
+
type: "text",
|
|
1436
|
+
text: `${summary.join(', ')}:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1437
|
+
}],
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
catch (error) {
|
|
1441
|
+
console.error("Error syncing work items from files:", error);
|
|
1442
|
+
return {
|
|
1443
|
+
content: [{
|
|
1444
|
+
type: "text",
|
|
1445
|
+
text: `Failed to push work items: ${error.message}`,
|
|
1446
|
+
}],
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
server.tool("check-work-item-markdown", "Check if work item fields are markdown (required for sync) or HTML format.", {
|
|
1451
|
+
project: z.string().describe("The project name"),
|
|
1452
|
+
workItemIds: z.array(z.number()).describe("Work item IDs to check"),
|
|
1453
|
+
}, async ({ project, workItemIds }) => {
|
|
1454
|
+
try {
|
|
1455
|
+
const service = getAzureDevOpsService();
|
|
1456
|
+
const results = [];
|
|
1457
|
+
let readyCount = 0;
|
|
1458
|
+
let needsConversionCount = 0;
|
|
1459
|
+
for (const workItemId of workItemIds) {
|
|
1460
|
+
try {
|
|
1461
|
+
const workItem = await service.getWorkItem(project, workItemId);
|
|
1462
|
+
const formats = checkFieldFormats(workItem);
|
|
1463
|
+
results.push({
|
|
1464
|
+
id: workItemId,
|
|
1465
|
+
description: formats.description,
|
|
1466
|
+
acceptanceCriteria: formats.acceptanceCriteria,
|
|
1467
|
+
howToTest: formats.additionalFields.howToTest,
|
|
1468
|
+
predeploymentSteps: formats.additionalFields.predeploymentSteps,
|
|
1469
|
+
postdeploymentSteps: formats.additionalFields.postdeploymentSteps,
|
|
1470
|
+
deploymentInformation: formats.additionalFields.deploymentInformation,
|
|
1471
|
+
ready: formats.ready,
|
|
1472
|
+
...(formats.warnings.length > 0 ? { warnings: formats.warnings } : {}),
|
|
1473
|
+
});
|
|
1474
|
+
if (formats.ready) {
|
|
1475
|
+
readyCount++;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
needsConversionCount++;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
results.push({
|
|
1483
|
+
id: workItemId,
|
|
1484
|
+
description: 'error',
|
|
1485
|
+
acceptanceCriteria: 'error',
|
|
1486
|
+
ready: false,
|
|
1487
|
+
error: error.message || String(error),
|
|
1488
|
+
});
|
|
1489
|
+
needsConversionCount++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const result = {
|
|
1493
|
+
results,
|
|
1494
|
+
summary: {
|
|
1495
|
+
ready: readyCount,
|
|
1496
|
+
needsConversion: needsConversionCount,
|
|
1497
|
+
},
|
|
1498
|
+
autoConvertAvailable: true,
|
|
1499
|
+
message: needsConversionCount === 0
|
|
1500
|
+
? 'All work items are markdown format - ready to sync'
|
|
1501
|
+
: `${needsConversionCount} work item(s) have HTML fields. Will be auto-converted to markdown on sync (unless skipAutoConvert=true).`,
|
|
1502
|
+
};
|
|
1503
|
+
return {
|
|
1504
|
+
content: [{
|
|
1505
|
+
type: "text",
|
|
1506
|
+
text: `Work item format check:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1507
|
+
}],
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
catch (error) {
|
|
1511
|
+
console.error("Error checking work item formats:", error);
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{
|
|
1514
|
+
type: "text",
|
|
1515
|
+
text: `Failed to check work item formats: ${error.message}`,
|
|
1516
|
+
}],
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
server.tool("list-synced-work-items", "List work items that have been synced to local markdown files.", {
|
|
1521
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1522
|
+
}, async ({ folder }) => {
|
|
1523
|
+
try {
|
|
1524
|
+
const syncConfig = getSyncConfig(folder);
|
|
1525
|
+
// Validate folder path for security
|
|
1526
|
+
validateFolderPath(syncConfig.folder);
|
|
1527
|
+
const workItems = await listSyncedWorkItems(syncConfig.folder);
|
|
1528
|
+
const result = {
|
|
1529
|
+
workItems,
|
|
1530
|
+
folder: syncConfig.folder,
|
|
1531
|
+
count: workItems.length,
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
content: [{
|
|
1535
|
+
type: "text",
|
|
1536
|
+
text: `Synced work items in ${syncConfig.folder}:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1537
|
+
}],
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
console.error("Error listing synced work items:", error);
|
|
1542
|
+
return {
|
|
1543
|
+
content: [{
|
|
1544
|
+
type: "text",
|
|
1545
|
+
text: `Failed to list synced work items: ${error.message}`,
|
|
1546
|
+
}],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
server.tool("create-user-story-file", "Create a new user story template file locally. The file can be edited and then pushed to ADO using sync-work-item-from-file.", {
|
|
1551
|
+
project: z.string().describe("The project name"),
|
|
1552
|
+
parentId: z.number().describe("Parent Feature ID - the new user story will be created under this feature"),
|
|
1553
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1554
|
+
}, async ({ project, parentId, folder }) => {
|
|
1555
|
+
try {
|
|
1556
|
+
const service = getAzureDevOpsService();
|
|
1557
|
+
const syncConfig = getSyncConfig(folder);
|
|
1558
|
+
// Validate folder path for security
|
|
1559
|
+
validateFolderPath(syncConfig.folder);
|
|
1560
|
+
// Fetch parent work item to get title
|
|
1561
|
+
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1562
|
+
const parentTitle = parentWorkItem.fields?.['System.Title'] || '';
|
|
1563
|
+
// Find next available index for new file
|
|
1564
|
+
const nextIndex = await findNextNewFileIndex(syncConfig.folder, parentId);
|
|
1565
|
+
const filePath = getNewWorkItemFilePath(syncConfig.folder, parentId, nextIndex);
|
|
1566
|
+
// Generate template content
|
|
1567
|
+
const template = generateNewWorkItemTemplate(parentId, parentTitle, project, 'User Story');
|
|
1568
|
+
// Ensure folder exists and write file
|
|
1569
|
+
await ensureFolderExists(syncConfig.folder);
|
|
1570
|
+
await writeWorkItemFile(filePath, template);
|
|
1571
|
+
const result = {
|
|
1572
|
+
file: filePath,
|
|
1573
|
+
parentId,
|
|
1574
|
+
parentTitle,
|
|
1575
|
+
instructions: [
|
|
1576
|
+
`1. Edit the file to update title, description, and acceptance criteria`,
|
|
1577
|
+
`2. Run sync-work-item-from-file(project: "${project}") to create in ADO`,
|
|
1578
|
+
`3. The file will be renamed to {newId}.md after creation`,
|
|
1579
|
+
],
|
|
1580
|
+
};
|
|
1581
|
+
return {
|
|
1582
|
+
content: [{
|
|
1583
|
+
type: "text",
|
|
1584
|
+
text: `Created new user story template:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1585
|
+
}],
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
console.error("Error creating user story file:", error);
|
|
1590
|
+
return {
|
|
1591
|
+
content: [{
|
|
1592
|
+
type: "text",
|
|
1593
|
+
text: `Failed to create user story file: ${error.message}`,
|
|
1594
|
+
}],
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1594
1598
|
// ========================================
|
|
1595
|
-
//
|
|
1599
|
+
// TASK SYNC TOOLS
|
|
1596
1600
|
// ========================================
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1601
|
+
server.tool("sync-tasks-to-file", descWithExamples("Download all tasks under a parent work item (User Story) to a local markdown file. Auto-converts HTML descriptions to markdown. Supports pulling tasks for multiple parents at once.", SYNC_TASKS_TO_FILE_EXAMPLES), {
|
|
1602
|
+
project: z.string().describe("The project name"),
|
|
1603
|
+
parentIds: z.array(z.number()).describe("Parent work item IDs (User Stories) to fetch tasks for"),
|
|
1604
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1605
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1606
|
+
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1607
|
+
try {
|
|
1608
|
+
const service = getAzureDevOpsService();
|
|
1609
|
+
const syncConfig = getSyncConfig(folder);
|
|
1610
|
+
// Validate folder path for security
|
|
1611
|
+
validateFolderPath(syncConfig.folder);
|
|
1612
|
+
const pulled = [];
|
|
1613
|
+
const failed = [];
|
|
1614
|
+
const filesToCommit = [];
|
|
1615
|
+
await ensureFolderExists(syncConfig.folder);
|
|
1616
|
+
for (const parentId of parentIds) {
|
|
1617
|
+
try {
|
|
1618
|
+
// Fetch parent work item to get title and validate existence
|
|
1619
|
+
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1620
|
+
// Query for child tasks using WIQL
|
|
1621
|
+
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.Parent] = ${parentId} AND [System.WorkItemType] = 'Task' ORDER BY [System.Id] ASC`;
|
|
1622
|
+
const queryResult = await service.queryWorkItems(project, wiql, 200);
|
|
1623
|
+
// Fetch full details for each task and auto-convert HTML descriptions
|
|
1624
|
+
const tasks = [];
|
|
1625
|
+
let tasksConverted = 0;
|
|
1626
|
+
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
1627
|
+
for (const wi of queryResult.workItems) {
|
|
1628
|
+
try {
|
|
1629
|
+
let task = await service.getWorkItem(project, wi.id);
|
|
1630
|
+
// Auto-convert HTML description to markdown unless skipAutoConvert
|
|
1631
|
+
const description = task.fields?.['System.Description'];
|
|
1632
|
+
if (description && !skipAutoConvert && isHtmlContent(description)) {
|
|
1633
|
+
try {
|
|
1634
|
+
await autoConvertFieldsToMarkdown(service, project, wi.id, task.fields, ['System.Description']);
|
|
1635
|
+
// Re-fetch with converted content
|
|
1636
|
+
task = await service.getWorkItem(project, wi.id);
|
|
1637
|
+
tasksConverted++;
|
|
1638
|
+
}
|
|
1639
|
+
catch (convertError) {
|
|
1640
|
+
console.error(`Failed to convert HTML description for task ${wi.id}:`, convertError.message);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
tasks.push(task);
|
|
1644
|
+
}
|
|
1645
|
+
catch (taskError) {
|
|
1646
|
+
console.error(`Error fetching task ${wi.id}:`, taskError.message);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
// Convert to markdown and save
|
|
1651
|
+
const markdown = tasksToMarkdown(parentWorkItem, tasks, project);
|
|
1652
|
+
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1653
|
+
await writeWorkItemFile(filePath, markdown);
|
|
1654
|
+
pulled.push({
|
|
1655
|
+
parentId,
|
|
1656
|
+
file: filePath,
|
|
1657
|
+
taskCount: tasks.length,
|
|
1658
|
+
});
|
|
1659
|
+
filesToCommit.push({ filePath, workItemId: parentId });
|
|
1660
|
+
}
|
|
1661
|
+
catch (error) {
|
|
1662
|
+
failed.push({ parentId, error: error.message });
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
// Auto-commit if enabled
|
|
1666
|
+
let committed = false;
|
|
1667
|
+
if (syncConfig.autoCommit && filesToCommit.length > 0) {
|
|
1668
|
+
const commitResult = await autoCommitMultipleFiles(filesToCommit, 'synced tasks for');
|
|
1669
|
+
committed = commitResult.committed;
|
|
1670
|
+
}
|
|
1671
|
+
const result = {
|
|
1672
|
+
pulled,
|
|
1673
|
+
failed,
|
|
1674
|
+
folder: syncConfig.folder,
|
|
1675
|
+
committed,
|
|
1676
|
+
};
|
|
1677
|
+
return {
|
|
1678
|
+
content: [{
|
|
1679
|
+
type: "text",
|
|
1680
|
+
text: `Synced tasks for ${pulled.length} parent(s) to local files:\n\n${JSON.stringify(result, null, 2)}`,
|
|
1681
|
+
}],
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
catch (error) {
|
|
1685
|
+
console.error("Error syncing tasks to files:", error);
|
|
1686
|
+
return {
|
|
1687
|
+
content: [{
|
|
1688
|
+
type: "text",
|
|
1689
|
+
text: `Failed to sync tasks: ${error.message}`,
|
|
1690
|
+
}],
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
server.tool("sync-tasks-from-file", descWithExamples("Push local task changes back to ADO with upsert semantics. Existing tasks (## Task #ID) are updated, new tasks (## NEW TASK) are created. Auto-converts HTML fields to markdown. Requires AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true.", SYNC_TASKS_FROM_FILE_EXAMPLES), {
|
|
1695
|
+
project: z.string().describe("The project name"),
|
|
1696
|
+
parentIds: z.array(z.number()).describe("Parent work item IDs to sync tasks for"),
|
|
1697
|
+
folder: z.string().optional().describe("Override folder path (default: docs/user-stories or AZUREDEVOPS_SYNC_FOLDER)"),
|
|
1698
|
+
skipAutoConvert: z.boolean().optional().describe("Skip automatic HTML-to-markdown conversion for task descriptions. Only use when explicitly requested. Default: false"),
|
|
1699
|
+
}, async ({ project, parentIds, folder, skipAutoConvert }) => {
|
|
1700
|
+
try {
|
|
1701
|
+
const service = getAzureDevOpsService();
|
|
1702
|
+
const syncConfig = getSyncConfig(folder);
|
|
1703
|
+
// Validate folder path for security
|
|
1704
|
+
validateFolderPath(syncConfig.folder);
|
|
1705
|
+
const updated = [];
|
|
1706
|
+
const created = [];
|
|
1707
|
+
const failed = [];
|
|
1708
|
+
for (const parentId of parentIds) {
|
|
1709
|
+
const filePath = getTasksFilePath(syncConfig.folder, parentId);
|
|
1710
|
+
try {
|
|
1711
|
+
// Check if file exists
|
|
1712
|
+
if (!await fileExists(filePath)) {
|
|
1713
|
+
failed.push({ parentId, error: `File not found: ${filePath}` });
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
// Parse local file
|
|
1717
|
+
const content = await readFileContent(filePath);
|
|
1718
|
+
const parsed = parseTasksMarkdown(content);
|
|
1719
|
+
// Get parent work item for area/iteration path
|
|
1720
|
+
const parentWorkItem = await service.getWorkItem(project, parentId);
|
|
1721
|
+
const parentFields = parentWorkItem.fields || {};
|
|
1722
|
+
const areaPath = parentFields['System.AreaPath'];
|
|
1723
|
+
const iterationPath = parentFields['System.IterationPath'];
|
|
1724
|
+
const createdInThisFile = [];
|
|
1725
|
+
// Process each task
|
|
1726
|
+
for (const task of parsed.tasks) {
|
|
1727
|
+
try {
|
|
1728
|
+
if (task.id !== null) {
|
|
1729
|
+
// Existing task - update (auto-converts HTML fields unless skipAutoConvert)
|
|
1730
|
+
const currentTask = await service.getWorkItem(project, task.id);
|
|
1731
|
+
const { operations, fieldsUpdated, convertedFields } = buildTaskPatchOperations(task, currentTask, skipAutoConvert);
|
|
1732
|
+
if (operations.length > 0) {
|
|
1733
|
+
await service.updateWorkItem(project, task.id, operations);
|
|
1734
|
+
updated.push({
|
|
1735
|
+
id: task.id,
|
|
1736
|
+
parentId,
|
|
1737
|
+
fieldsUpdated,
|
|
1738
|
+
...(convertedFields.length > 0 ? { convertedFields } : {}),
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
// No changes needed
|
|
1743
|
+
updated.push({
|
|
1744
|
+
id: task.id,
|
|
1745
|
+
parentId,
|
|
1746
|
+
fieldsUpdated: [],
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
// New task - create
|
|
1752
|
+
if (!task.title) {
|
|
1753
|
+
continue; // Skip tasks without title
|
|
1754
|
+
}
|
|
1755
|
+
const fields = buildNewTaskFields(task, areaPath, iterationPath);
|
|
1756
|
+
const createdTask = await service.createWorkItem(project, 'Task', fields, parentId);
|
|
1757
|
+
created.push({
|
|
1758
|
+
id: createdTask.id,
|
|
1759
|
+
parentId,
|
|
1760
|
+
title: task.title,
|
|
1761
|
+
});
|
|
1762
|
+
createdInThisFile.push({
|
|
1763
|
+
title: task.title,
|
|
1764
|
+
id: createdTask.id,
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
catch (taskError) {
|
|
1769
|
+
failed.push({
|
|
1770
|
+
parentId,
|
|
1771
|
+
taskId: task.id || undefined,
|
|
1772
|
+
error: taskError.message,
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// Update the file with new task IDs if any were created
|
|
1777
|
+
if (createdInThisFile.length > 0) {
|
|
1778
|
+
const updatedContent = updateTasksFileAfterCreate(content, createdInThisFile);
|
|
1779
|
+
await writeWorkItemFile(filePath, updatedContent);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
failed.push({ parentId, error: error.message });
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const result = {
|
|
1787
|
+
updated,
|
|
1788
|
+
created,
|
|
1789
|
+
failed,
|
|
1790
|
+
folder: syncConfig.folder,
|
|
1791
|
+
};
|
|
1792
|
+
return {
|
|
1793
|
+
content: [{
|
|
1794
|
+
type: "text",
|
|
1795
|
+
text: `Pushed tasks to ADO (${updated.length} updated, ${created.length} created):\n\n${JSON.stringify(result, null, 2)}`,
|
|
1796
|
+
}],
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
console.error("Error syncing tasks from files:", error);
|
|
1801
|
+
return {
|
|
1802
|
+
content: [{
|
|
1803
|
+
type: "text",
|
|
1804
|
+
text: `Failed to push tasks: ${error.message}`,
|
|
1805
|
+
}],
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
// Log registration summary (enablePullRequestWrite already defined above)
|
|
1810
|
+
// Tool count breakdown:
|
|
1811
|
+
// - Wiki: 6 (get-wikis, search-wiki-pages, get-wiki-page, create-wiki-page, update-wiki-page, str-replace-wiki-page)
|
|
1812
|
+
// - Work Item: 7 (get-work-item, query-work-items, get-work-item-comments, add-work-item-comment, update-work-item, create-work-item, delete-work-item)
|
|
1813
|
+
// - Variable Group: 2 (list-variable-groups, get-variable-group)
|
|
1814
|
+
// - PR Read-only: 6 (list-repositories, list-pull-requests, get-pull-request, get-pull-request-threads, get-pull-request-commits, get-pull-request-changes)
|
|
1815
|
+
// - Build Troubleshooting: 3 (get-build-status, get-build-timeline, get-build-logs)
|
|
1816
|
+
// - Work Item Sync: 5 (sync-work-item-to-file, sync-work-item-from-file, check-work-item-markdown, list-synced-work-items, create-user-story-file)
|
|
1817
|
+
// - Task Sync: 2 (sync-tasks-to-file, sync-tasks-from-file)
|
|
1818
|
+
// - Config: 1 (get-configuration)
|
|
1819
|
+
// Total base: 6 + 7 + 2 + 6 + 3 + 5 + 2 + 1 = 32
|
|
1820
|
+
// + PR Write: 7 (add-pull-request-thread, create-pull-request, update-pull-request, complete-pull-request, add-pr-reviewer, vote-pull-request, reply-to-pr-thread) - conditional
|
|
1821
|
+
const baseToolsCount = 32;
|
|
1822
|
+
const prWriteToolsCount = enablePullRequestWrite ? 7 : 0;
|
|
1823
|
+
const totalToolsCount = baseToolsCount + prWriteToolsCount;
|
|
1824
|
+
console.error(`azure-devops tools registered: ${totalToolsCount} tools (${baseToolsCount} base + ${prWriteToolsCount} PR write), 4 prompts`);
|
|
1825
|
+
// NOTE: Admin tools (pipelines, service connections, agent pools, environments)
|
|
1826
|
+
// have been moved to the @mcp-consultant-tools/azure-devops-admin package
|
|
1633
1827
|
}
|
|
1634
1828
|
/**
|
|
1635
1829
|
* Export service class for direct usage
|