Skip to content

Commit 5b842c5

Browse files
authored
feat: Update work item comment format to default to Markdown (#1155)
This pull request standardizes the handling of the `format` parameter for work item comments and field values in the Azure DevOps integration. The default format is now consistently set to `"Markdown"` instead of `"Html"` across all relevant tools, and the code logic and tests have been updated to use the correct casing and default values. This ensures more predictable behavior and aligns the API with user expectations. ## GitHub issue number #1154 ## **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?** manual testing. updated automated tests to account for empty
1 parent 5041c99 commit 5b842c5

File tree

2 files changed

+43
-11
lines changed

2 files changed

+43
-11
lines changed

src/tools/work-items.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
356356
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
357357
workItemId: z.coerce.number().min(1).describe("The ID of the work item to add a comment to."),
358358
comment: z.string().describe("The text of the comment to add to the work item."),
359-
format: z.enum(["markdown", "html"]).optional().default("html"),
359+
format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
360360
},
361361
async ({ project, workItemId, comment, format }) => {
362362
try {
@@ -376,7 +376,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
376376
text: comment,
377377
};
378378

379-
const formatParameter = format === "markdown" ? 0 : 1;
379+
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
380380
const response = await fetch(
381381
`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`,
382382
{
@@ -417,7 +417,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
417417
workItemId: z.coerce.number().min(1).describe("The ID of the work item."),
418418
commentId: z.coerce.number().min(1).describe("The ID of the comment to update."),
419419
text: z.string().describe("The updated comment text."),
420-
format: z.enum(["markdown", "html"]).optional().default("html"),
420+
format: z.enum(["Markdown", "Html"]).optional().default("Markdown").describe("The format of the comment text, e.g., 'Markdown', 'Html'. Optional, defaults to 'Markdown'."),
421421
},
422422
async ({ project, workItemId, commentId, text, format }) => {
423423
try {
@@ -433,8 +433,8 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
433433
const orgUrl = connection.serverUrl;
434434
const accessToken = await tokenProvider();
435435
const body: Record<string, string> = { text };
436+
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
436437

437-
const formatParameter = format === "markdown" ? 0 : 1;
438438
const response = await fetch(
439439
`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`,
440440
{
@@ -547,7 +547,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
547547
z.object({
548548
title: z.string().describe("The title of the child work item."),
549549
description: z.string().describe("The description of the child work item."),
550-
format: z.enum(["Markdown", "Html"]).default("Html").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Html'."),
550+
format: z.enum(["Markdown", "Html"]).default("Markdown").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Markdown'."),
551551
areaPath: z.string().optional().describe("Optional area path for the child work item."),
552552
iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
553553
})
@@ -877,7 +877,7 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
877877
z.object({
878878
name: z.string().describe("The name of the field, e.g., 'System.Title'."),
879879
value: z.string().describe("The value of the field."),
880-
format: z.enum(["Html", "Markdown"]).optional().describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
880+
format: z.enum(["Html", "Markdown"]).optional().default("Markdown").describe("the format of the field value, e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
881881
})
882882
)
883883
.describe("A record of field names and values to set on the new work item. Each fild is the field name and each value is the corresponding value to set for that field."),
@@ -1027,7 +1027,11 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
10271027
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
10281028
path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
10291029
value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
1030-
format: z.enum(["Html", "Markdown"]).optional().describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Html'."),
1030+
format: z
1031+
.enum(["Html", "Markdown"])
1032+
.optional()
1033+
.default("Markdown")
1034+
.describe("The format of the field value. Only to be used for large text fields. e.g., 'Html', 'Markdown'. Optional, defaults to 'Markdown'."),
10311035
})
10321036
)
10331037
.describe("An array of updates to apply to work items. Each update should include the operation (op), work item ID (id), field path (path), and new value (value)."),

test/src/tools/work-items.test.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ describe("configureWorkItemTools", () => {
819819
const result = await handler(params);
820820

821821
expect(mockFetch).toHaveBeenCalledWith(
822-
"https://dev.azure.com/contoso/Contoso/_apis/wit/workItems/299/comments?format=1&api-version=7.2-preview.4",
822+
"https://dev.azure.com/contoso/Contoso/_apis/wit/workItems/299/comments?format=0&api-version=7.2-preview.4",
823823
expect.objectContaining({
824824
method: "POST",
825825
headers: expect.objectContaining({
@@ -854,7 +854,7 @@ describe("configureWorkItemTools", () => {
854854
comment: "hello world!",
855855
project: "Contoso",
856856
workItemId: 299,
857-
format: "markdown",
857+
format: "Markdown",
858858
};
859859

860860
const result = await handler(params);
@@ -873,6 +873,23 @@ describe("configureWorkItemTools", () => {
873873
expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItemComment));
874874
});
875875

876+
it("should call Add Work Item Comments API with format=1 when format is Html", async () => {
877+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
878+
879+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_work_item_comment");
880+
if (!call) throw new Error("wit_add_work_item_comment tool not registered");
881+
const [, , , handler] = call;
882+
883+
mockConnection.serverUrl = "https://dev.azure.com/contoso";
884+
(tokenProvider as jest.Mock).mockResolvedValue("fake-token");
885+
const mockFetch = jest.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(JSON.stringify(_mockWorkItemComment)) });
886+
global.fetch = mockFetch;
887+
888+
await handler({ comment: "hello world!", project: "Contoso", workItemId: 299, format: "Html" });
889+
890+
expect(mockFetch).toHaveBeenCalledWith("https://dev.azure.com/contoso/Contoso/_apis/wit/workItems/299/comments?format=1&api-version=7.2-preview.4", expect.objectContaining({ method: "POST" }));
891+
});
892+
876893
it("should handle fetch failure response", async () => {
877894
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
878895

@@ -971,7 +988,7 @@ describe("configureWorkItemTools", () => {
971988
const result = await handler(params);
972989

973990
expect(mockFetch).toHaveBeenCalledWith(
974-
"https://dev.azure.com/contoso/TestProject/_apis/wit/workItems/42/comments/100?format=1&api-version=7.2-preview.4",
991+
"https://dev.azure.com/contoso/TestProject/_apis/wit/workItems/42/comments/100?format=0&api-version=7.2-preview.4",
975992
expect.objectContaining({
976993
method: "PATCH",
977994
headers: expect.objectContaining({
@@ -4605,11 +4622,22 @@ describe("configureWorkItemTools", () => {
46054622
(tokenProvider as jest.Mock).mockResolvedValue("fake-token");
46064623
global.fetch = jest.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve("{}") });
46074624

4608-
await handler({ project: "P", workItemId: 1, commentId: 1, text: "updated", format: "markdown" });
4625+
await handler({ project: "P", workItemId: 1, commentId: 1, text: "updated", format: "Markdown" });
46094626
const calledUrl = (global.fetch as jest.Mock).mock.calls[0][0] as string;
46104627
expect(calledUrl).toContain("format=0");
46114628
});
46124629

4630+
it("update_work_item_comment: should use format=1 when format is Html", async () => {
4631+
const handler = getHandler("wit_update_work_item_comment");
4632+
mockConnection.serverUrl = "https://dev.azure.com/contoso";
4633+
(tokenProvider as jest.Mock).mockResolvedValue("fake-token");
4634+
global.fetch = jest.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve("{}") });
4635+
4636+
await handler({ project: "P", workItemId: 1, commentId: 1, text: "updated", format: "Html" });
4637+
const calledUrl = (global.fetch as jest.Mock).mock.calls[0][0] as string;
4638+
expect(calledUrl).toContain("format=1");
4639+
});
4640+
46134641
it("list_work_item_revisions: should return unknown error message for non-Error throws", async () => {
46144642
const handler = getHandler("wit_list_work_item_revisions");
46154643
(connectionProvider as jest.Mock).mockRejectedValue("string error");

0 commit comments

Comments
 (0)