Skip to content

[feat]: Add wiki creation and update functionality #374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8fb63de
Add wiki creation and update functionality
ssmith-avidxchange Jul 31, 2025
85970aa
Enhance wiki functionality and documentation
ssmith-avidxchange Jul 31, 2025
8424a71
Add tests for ETag handling in wiki page updates
ssmith-avidxchange Jul 31, 2025
9f1e87c
Fix ETag string formatting in wiki tests for consistency
ssmith-avidxchange Jul 31, 2025
2a13b09
Add comprehensive tests for error handling in wiki page updates
ssmith-avidxchange Jul 31, 2025
60296c7
Merge branch 'main' into users/ssmith/wiki-create-update-features
danhellem Aug 1, 2025
915b280
Merge branch 'main' into users/ssmith/wiki-create-update-features
danhellem Aug 4, 2025
18cbc78
Remove console log statements from wiki page creation and update func…
ssmith-avidxchange Aug 5, 2025
b2d0ac5
Merge branch 'users/ssmith/wiki-create-update-features' of https://gi…
ssmith-avidxchange Aug 5, 2025
be0a978
Merge branch 'main' into users/ssmith/wiki-create-update-features
ssmith-avidxchange Aug 5, 2025
c377926
Merge branch 'main' into users/ssmith/wiki-create-update-features
ssmith-avidxchange Aug 6, 2025
c8a8192
Merge branch 'main' into users/ssmith/wiki-create-update-features
danhellem Aug 11, 2025
4a79e88
Merge branch 'main' into users/ssmith/wiki-create-update-features
danhellem Aug 12, 2025
49d674a
Simplify wiki tools: Remove create_wiki and update_wiki tools, keep o…
ssmith-avidxchange Aug 13, 2025
27fd3fa
Merge branch 'main' into users/ssmith/wiki-create-update-features
ssmith-avidxchange Aug 13, 2025
8842230
Update README.md: Remove references to deleted wiki_create_wiki and w…
ssmith-avidxchange Aug 13, 2025
6387341
Merge branch 'main' into users/ssmith/wiki-create-update-features
danhellem Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,25 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
- "List iterations for project 'Contoso'"
- "List my work items for project 'Contoso'"
- "List work items in current iteration for 'Contoso' project and 'Contoso Team'"
- "List all wikis in the 'Contoso' project"
- "Create a wiki page '/Architecture/Overview' with content about system design"
- "Update the wiki page '/Getting Started' with new onboarding instructions"
- "Get the content of the wiki page '/API/Authentication' from the Documentation wiki"

## πŸ† Expectations

The Azure DevOps MCP Server is built from tools that are concise, simple, focused, and easy to useβ€”each designed for a specific scenario. We intentionally avoid complex tools that try to do too much. The goal is to provide a thin abstraction layer over the REST APIs, making data access straightforward and letting the language model handle complex reasoning.

## ✨ Recent Enhancements

### πŸ“– **Enhanced Wiki Support**

- **Full Content Management**: Create and update wiki pages with complete content using the native Azure DevOps REST API
- **Automatic ETag Handling**: Safe updates with built-in conflict resolution for concurrent edits
- **Immediate Visibility**: Pages appear instantly in the Azure DevOps wiki interface
- **Hierarchical Structure**: Support for organized page structures within existing folder hierarchies
- **Robust Error Handling**: Comprehensive error management for various HTTP status codes and edge cases

## βš™οΈ Supported Tools

Interact with these Azure DevOps services:
Expand Down Expand Up @@ -133,6 +147,14 @@ Interact with these Azure DevOps services:
- **testplan_list_test_cases**: Get a list of test cases in the test plan.
- **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.

### πŸ“– Wiki

- **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
- **wiki_get_wiki**: Get the wiki by wikiIdentifier.
- **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
- **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
- **wiki_create_or_update_page**: ✨ **Enhanced** - Create or update wiki pages with full content support using Azure DevOps REST API. Features automatic ETag handling for safe updates, immediate content visibility, and proper conflict resolution.

### πŸ”Ž Search

- **search_code**: Get code search results for a given search text.
Expand Down
119 changes: 119 additions & 0 deletions src/tools/wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const WIKI_TOOLS = {
get_wiki: "wiki_get_wiki",
list_wiki_pages: "wiki_list_pages",
get_wiki_page_content: "wiki_get_page_content",
create_or_update_page: "wiki_create_or_update_page",
};

function configureWikiTools(server: McpServer, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>) {
Expand Down Expand Up @@ -151,6 +152,124 @@ function configureWikiTools(server: McpServer, tokenProvider: () => Promise<Acce
}
}
);

server.tool(
WIKI_TOOLS.create_or_update_page,
"Create or update a wiki page with content.",
{
wikiIdentifier: z.string().describe("The unique identifier or name of the wiki."),
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
content: z.string().describe("The content of the wiki page in markdown format."),
project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."),
comment: z.string().optional().describe("Optional comment for the page update."),
etag: z.string().optional().describe("ETag for editing existing pages (optional, will be fetched if not provided)."),
},
async ({ wikiIdentifier, path, content, project, etag }) => {
try {
const connection = await connectionProvider();
const accessToken = await tokenProvider();

// Normalize the path
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const encodedPath = encodeURIComponent(normalizedPath);

// Build the URL for the wiki page API
const baseUrl = connection.serverUrl;
const projectParam = project || "";
const url = `${baseUrl}/${projectParam}/_apis/wiki/wikis/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.1`;

// First, try to create a new page (PUT without ETag)
try {
const createResponse = await fetch(url, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: content }),
});

if (createResponse.ok) {
const result = await createResponse.json();
return {
content: [
{
type: "text",
text: `Successfully created wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
},
],
};
}

// If creation failed with 409 (Conflict) or 500 (Page exists), try to update it
if (createResponse.status === 409 || createResponse.status === 500) {
// Page exists, we need to get the ETag and update it
let currentEtag = etag;

if (!currentEtag) {
// Fetch current page to get ETag
const getResponse = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken.token}`,
},
});

if (getResponse.ok) {
currentEtag = getResponse.headers.get("etag") || getResponse.headers.get("ETag") || undefined;
if (!currentEtag) {
const pageData = await getResponse.json();
currentEtag = pageData.eTag;
}
}

if (!currentEtag) {
throw new Error("Could not retrieve ETag for existing page");
}
}

// Now update the existing page with ETag
const updateResponse = await fetch(url, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken.token}`,
"Content-Type": "application/json",
"If-Match": currentEtag,
},
body: JSON.stringify({ content: content }),
});

if (updateResponse.ok) {
const result = await updateResponse.json();
return {
content: [
{
type: "text",
text: `Successfully updated wiki page at path: ${normalizedPath}. Response: ${JSON.stringify(result, null, 2)}`,
},
],
};
} else {
const errorText = await updateResponse.text();
throw new Error(`Failed to update page (${updateResponse.status}): ${errorText}`);
}
} else {
const errorText = await createResponse.text();
throw new Error(`Failed to create page (${createResponse.status}): ${errorText}`);
}
} catch (fetchError) {
throw fetchError;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";

return {
content: [{ type: "text", text: `Error creating/updating wiki page: ${errorMessage}` }],
isError: true,
};
}
}
);
}

function streamToString(stream: NodeJS.ReadableStream): Promise<string> {
Expand Down
Loading
Loading