Skip to content

Commit 2cf6765

Browse files
polatengindanhellem
authored andcommitted
Add functionality to link artifacts to work items (microsoft#377)
Adds a new `wit_add_artifact_link` tool to enable linking work items to repository artifacts (`branches`, `commits`, `pull requests`). ## 🔧 Core Implementation Added `wit_add_artifact_link` tool in [workitems.ts](https://vscode.dev/github/microsoft/azure-devops-mcp/blob/main/src/tools/workitems.ts) ## 📚 Documentation Updated [EXAMPLES.md](https://vscode.dev/github/microsoft/azure-devops-mcp/blob/main/docs/EXAMPLES.md) with new "Adding Artifact Links" section ## ✅ Testing Added test coverage in [workitems.test.ts](https://vscode.dev/github/microsoft/azure-devops-mcp/blob/main/test/src/tools/workitems.test.ts) ## GitHub issue number microsoft#279 ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Run `npm run test` , `npm run build` , `npm run validate-tools` --------- Co-authored-by: Dan Hellem <[email protected]>
1 parent f553be0 commit 2cf6765

File tree

3 files changed

+291
-1
lines changed

3 files changed

+291
-1
lines changed

docs/EXAMPLES.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ This guide offers step-by-step examples for using the Azure DevOps MCP Server to
1010
[Create and Link Test Cases](#create-and-link-test-cases)<br/>
1111
[Triage Work](#triage-work)<br/>
1212
[Using Markdown Format](#adding-and-updating-work-items-using-the-format-paramater)<br/>
13-
[Remove Links from a Work Item](#remove-one-or-more-links-from-a-work-item)
13+
[Remove Links from a Work Item](#remove-one-or-more-links-from-a-work-item)<br/>
14+
[Adding Artifact Links](#adding-artifact-links)
1415

1516
## 🙋‍♂️ Projects and Teams
1617

@@ -149,3 +150,21 @@ Next, remove a specific link to a work item, pull request, etc. or remove links
149150
```plaintext
150151
Remove link 5678 and 91011 from work item 1234. Also remove any related links and links to pull request 121314
151152
```
153+
154+
## 🔗 Adding Artifact Links
155+
156+
### Add Artifact Links to Work Items
157+
158+
Use this tool to link work items to repository artifacts like branches, commits, and pull requests. This is particularly useful for GitHub Copilot integration, which requires artifact links to understand repository context.
159+
160+
First, you'll need the proper vstfs URI format for your artifact:
161+
162+
- **Branch**: `vstfs:///Git/Ref/{projectId}%2F{repositoryId}%2FGB{branchName}`
163+
- **Commit**: `vstfs:///Git/Commit/{projectId}%2F{repositoryId}%2F{commitId}`
164+
- **Pull Request**: `vstfs:///Git/PullRequestId/{projectId}%2F{repositoryId}%2F{pullRequestId}`
165+
166+
```plaintext
167+
Add a branch artifact link to work item 1234 in project "Contoso" with URI "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain" and link type "Branch" with comment "Linked to main branch for GitHub Copilot integration"
168+
```
169+
170+
📽️ [Adding artifact links enables automation of work item creation and GitHub Copilot integration]()

src/tools/workitems.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const WORKITEM_TOOLS = {
2929
update_work_items_batch: "wit_update_work_items_batch",
3030
work_items_link: "wit_work_items_link",
3131
work_item_unlink: "wit_work_item_unlink",
32+
add_artifact_link: "wit_add_artifact_link",
3233
};
3334

3435
function getLinkTypeFromName(name: string) {
@@ -879,6 +880,160 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
879880
}
880881
}
881882
);
883+
884+
server.tool(
885+
WORKITEM_TOOLS.add_artifact_link,
886+
"Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically.",
887+
{
888+
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
889+
project: z.string().describe("The name or ID of the Azure DevOps project."),
890+
891+
// Option 1: Provide full URI directly
892+
artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
893+
894+
// Option 2: Provide individual components to build URI automatically based on linkType
895+
projectId: z.string().optional().describe("The project ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
896+
repositoryId: z.string().optional().describe("The repository ID (GUID) containing the artifact. Required for Git artifacts when artifactUri is not provided."),
897+
branchName: z.string().optional().describe("The branch name (e.g., 'main'). Required when linkType is 'Branch'."),
898+
commitId: z.string().optional().describe("The commit SHA hash. Required when linkType is 'Fixed in Commit'."),
899+
pullRequestId: z.number().optional().describe("The pull request ID. Required when linkType is 'Pull Request'."),
900+
buildId: z.number().optional().describe("The build ID. Required when linkType is 'Build', 'Found in build', or 'Integrated in build'."),
901+
902+
linkType: z
903+
.enum([
904+
"Branch",
905+
"Build",
906+
"Fixed in Changeset",
907+
"Fixed in Commit",
908+
"Found in build",
909+
"Integrated in build",
910+
"Model Link",
911+
"Pull Request",
912+
"Related Workitem",
913+
"Result Attachment",
914+
"Source Code File",
915+
"Tag",
916+
"Test Result",
917+
"Wiki",
918+
])
919+
.default("Branch")
920+
.describe("Type of artifact link, defaults to 'Branch'. This determines both the link type and how to build the VSTFS URI from individual components."),
921+
comment: z.string().optional().describe("Comment to include with the artifact link."),
922+
},
923+
async ({ workItemId, project, artifactUri, projectId, repositoryId, branchName, commitId, pullRequestId, buildId, linkType, comment }) => {
924+
try {
925+
const connection = await connectionProvider();
926+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
927+
928+
let finalArtifactUri: string;
929+
930+
if (artifactUri) {
931+
// Use the provided full URI
932+
finalArtifactUri = artifactUri;
933+
} else {
934+
// Build the URI from individual components based on linkType
935+
switch (linkType) {
936+
case "Branch":
937+
if (!projectId || !repositoryId || !branchName) {
938+
return {
939+
content: [{ type: "text", text: "For 'Branch' links, 'projectId', 'repositoryId', and 'branchName' are required." }],
940+
isError: true,
941+
};
942+
}
943+
finalArtifactUri = `vstfs:///Git/Ref/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2FGB${encodeURIComponent(branchName)}`;
944+
break;
945+
946+
case "Fixed in Commit":
947+
if (!projectId || !repositoryId || !commitId) {
948+
return {
949+
content: [{ type: "text", text: "For 'Fixed in Commit' links, 'projectId', 'repositoryId', and 'commitId' are required." }],
950+
isError: true,
951+
};
952+
}
953+
finalArtifactUri = `vstfs:///Git/Commit/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(commitId)}`;
954+
break;
955+
956+
case "Pull Request":
957+
if (!projectId || !repositoryId || pullRequestId === undefined) {
958+
return {
959+
content: [{ type: "text", text: "For 'Pull Request' links, 'projectId', 'repositoryId', and 'pullRequestId' are required." }],
960+
isError: true,
961+
};
962+
}
963+
finalArtifactUri = `vstfs:///Git/PullRequestId/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${encodeURIComponent(pullRequestId.toString())}`;
964+
break;
965+
966+
case "Build":
967+
case "Found in build":
968+
case "Integrated in build":
969+
if (buildId === undefined) {
970+
return {
971+
content: [{ type: "text", text: `For '${linkType}' links, 'buildId' is required.` }],
972+
isError: true,
973+
};
974+
}
975+
finalArtifactUri = `vstfs:///Build/Build/${encodeURIComponent(buildId.toString())}`;
976+
break;
977+
978+
default:
979+
return {
980+
content: [{ type: "text", text: `URI building from components is not supported for link type '${linkType}'. Please provide the full 'artifactUri' instead.` }],
981+
isError: true,
982+
};
983+
}
984+
}
985+
986+
// Create the patch document for adding an artifact link relation
987+
const patchDocument = [
988+
{
989+
op: "add",
990+
path: "/relations/-",
991+
value: {
992+
rel: "ArtifactLink",
993+
url: finalArtifactUri,
994+
attributes: {
995+
name: linkType,
996+
...(comment && { comment }),
997+
},
998+
},
999+
},
1000+
];
1001+
1002+
// Use the WorkItem API to update the work item with the new relation
1003+
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
1004+
1005+
if (!workItem) {
1006+
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
1007+
}
1008+
1009+
return {
1010+
content: [
1011+
{
1012+
type: "text",
1013+
text: JSON.stringify(
1014+
{
1015+
workItemId,
1016+
artifactUri: finalArtifactUri,
1017+
linkType,
1018+
comment: comment || null,
1019+
success: true,
1020+
},
1021+
null,
1022+
2
1023+
),
1024+
},
1025+
],
1026+
};
1027+
} catch (error) {
1028+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
1029+
1030+
return {
1031+
content: [{ type: "text", text: `Error adding artifact link to work item: ${errorMessage}` }],
1032+
isError: true,
1033+
};
1034+
}
1035+
}
1036+
);
8821037
}
8831038

8841039
export { WORKITEM_TOOLS, configureWorkItemTools };

test/src/tools/workitems.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,4 +2318,120 @@ describe("configureWorkItemTools", () => {
23182318
expect(result.isError).toBe(true);
23192319
});
23202320
});
2321+
2322+
describe("artifact link tools", () => {
2323+
describe("wit_add_artifact_link", () => {
2324+
it("should add artifact link to work item successfully", async () => {
2325+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
2326+
2327+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
2328+
if (!call) throw new Error("wit_add_artifact_link tool not registered");
2329+
const [, , , handler] = call;
2330+
2331+
const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } };
2332+
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem);
2333+
2334+
const params = {
2335+
workItemId: 1234,
2336+
project: "TestProject",
2337+
artifactUri: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain",
2338+
linkType: "Branch",
2339+
comment: "Linked to main branch",
2340+
};
2341+
2342+
const result = await handler(params);
2343+
2344+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(
2345+
{},
2346+
[
2347+
{
2348+
op: "add",
2349+
path: "/relations/-",
2350+
value: {
2351+
rel: "ArtifactLink",
2352+
url: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain",
2353+
attributes: {
2354+
name: "Branch",
2355+
comment: "Linked to main branch",
2356+
},
2357+
},
2358+
},
2359+
],
2360+
1234,
2361+
"TestProject"
2362+
);
2363+
2364+
const response = JSON.parse(result.content[0].text);
2365+
expect(response.workItemId).toBe(1234);
2366+
expect(response.artifactUri).toBe("vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain");
2367+
expect(response.linkType).toBe("Branch");
2368+
expect(response.comment).toBe("Linked to main branch");
2369+
expect(response.success).toBe(true);
2370+
});
2371+
2372+
it("should add artifact link without comment", async () => {
2373+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
2374+
2375+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
2376+
if (!call) throw new Error("wit_add_artifact_link tool not registered");
2377+
const [, , , handler] = call;
2378+
2379+
const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } };
2380+
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem);
2381+
2382+
const params = {
2383+
workItemId: 1234,
2384+
project: "TestProject",
2385+
artifactUri: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123",
2386+
linkType: "Commit",
2387+
};
2388+
2389+
const result = await handler(params);
2390+
2391+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(
2392+
{},
2393+
[
2394+
{
2395+
op: "add",
2396+
path: "/relations/-",
2397+
value: {
2398+
rel: "ArtifactLink",
2399+
url: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123",
2400+
attributes: {
2401+
name: "Commit",
2402+
},
2403+
},
2404+
},
2405+
],
2406+
1234,
2407+
"TestProject"
2408+
);
2409+
2410+
const response = JSON.parse(result.content[0].text);
2411+
expect(response.comment).toBe(null);
2412+
});
2413+
2414+
it("should handle errors when adding artifact link", async () => {
2415+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
2416+
2417+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
2418+
if (!call) throw new Error("wit_add_artifact_link tool not registered");
2419+
const [, , , handler] = call;
2420+
2421+
mockWorkItemTrackingApi.updateWorkItem.mockRejectedValue(new Error("API Error"));
2422+
2423+
const params = {
2424+
workItemId: 1234,
2425+
project: "TestProject",
2426+
artifactUri: "vstfs:///Git/Ref/invalid",
2427+
linkType: "Branch",
2428+
};
2429+
2430+
const result = await handler(params);
2431+
2432+
expect(result.content[0].text).toBe("Error adding artifact link to work item: API Error");
2433+
expect(result.isError).toBe(true);
2434+
});
2435+
});
2436+
});
23212437
});

0 commit comments

Comments
 (0)