diff --git a/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiDto.cs b/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiDto.cs
new file mode 100644
index 0000000..ca12952
--- /dev/null
+++ b/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiDto.cs
@@ -0,0 +1,18 @@
+namespace Viamus.Azure.Devops.Mcp.Server.Models;
+
+///
+/// Data transfer object representing an Azure DevOps Wiki.
+///
+public sealed record WikiDto
+{
+ public string? Id { get; init; }
+ public string? Name { get; init; }
+ public string? Type { get; init; }
+ public string? Url { get; init; }
+ public string? RemoteUrl { get; init; }
+ public string? ProjectId { get; init; }
+ public string? ProjectName { get; init; }
+ public string? RepositoryId { get; init; }
+ public string? MappedPath { get; init; }
+ public List? Versions { get; init; }
+}
diff --git a/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiPageDto.cs b/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiPageDto.cs
new file mode 100644
index 0000000..ec6a063
--- /dev/null
+++ b/src/Viamus.Azure.Devops.Mcp.Server/Models/WikiPageDto.cs
@@ -0,0 +1,29 @@
+namespace Viamus.Azure.Devops.Mcp.Server.Models;
+
+///
+/// Data transfer object representing an Azure DevOps Wiki page.
+///
+public sealed record WikiPageDto
+{
+ public int? Id { get; init; }
+ public string? Path { get; init; }
+ public string? Content { get; init; }
+ public int? Order { get; init; }
+ public string? GitItemPath { get; init; }
+ public string? RemoteUrl { get; init; }
+ public bool IsParentPage { get; init; }
+ public List? SubPages { get; init; }
+}
+
+///
+/// Lightweight summary of a Wiki page for listing operations.
+///
+public sealed record WikiPageSummaryDto
+{
+ public int? Id { get; init; }
+ public string? Path { get; init; }
+ public int? Order { get; init; }
+ public string? GitItemPath { get; init; }
+ public string? RemoteUrl { get; init; }
+ public bool IsParentPage { get; init; }
+}
diff --git a/src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs b/src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs
index 38b9704..dabd7cc 100644
--- a/src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs
+++ b/src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Options;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.TeamFoundation.SourceControl.WebApi;
+using Microsoft.TeamFoundation.Wiki.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
@@ -23,6 +24,7 @@ public sealed class AzureDevOpsService : IAzureDevOpsService, IDisposable
private readonly WorkItemTrackingHttpClient _witClient;
private readonly GitHttpClient _gitClient;
private readonly BuildHttpClient _buildClient;
+ private readonly WikiHttpClient _wikiClient;
private bool _disposed;
private static readonly string[] DefaultFields =
@@ -71,6 +73,7 @@ public AzureDevOpsService(IOptions options, ILogger();
_gitClient = _connection.GetClient();
_buildClient = _connection.GetClient();
+ _wikiClient = _connection.GetClient();
_logger.LogInformation("Azure DevOps service initialized for organization: {OrganizationUrl}", _options.OrganizationUrl);
}
@@ -1628,6 +1631,174 @@ private static BuildTimelineRecordDto MapToBuildTimelineRecordDto(TimelineRecord
#endregion
+ #region Wiki Operations
+
+ public async Task> GetWikisAsync(string? project = null, CancellationToken cancellationToken = default)
+ {
+ var projectName = project ?? _options.DefaultProject;
+
+ _logger.LogInformation("Getting wikis for project: {Project}", projectName ?? "(all)");
+
+ try
+ {
+ var wikis = await _wikiClient.GetAllWikisAsync(project: projectName, cancellationToken: cancellationToken);
+
+ _logger.LogInformation("Found {Count} wikis", wikis.Count);
+
+ return wikis.Select(MapToWikiDto).ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting wikis for project: {Project}", projectName);
+ throw;
+ }
+ }
+
+ public async Task GetWikiAsync(string wikiIdentifier, string? project = null, CancellationToken cancellationToken = default)
+ {
+ var projectName = project ?? _options.DefaultProject;
+
+ _logger.LogInformation("Getting wiki: {WikiIdentifier} in project: {Project}", wikiIdentifier, projectName ?? "(default)");
+
+ try
+ {
+ var wiki = await _wikiClient.GetWikiAsync(project: projectName, wikiIdentifier: wikiIdentifier, cancellationToken: cancellationToken);
+ return MapToWikiDto(wiki);
+ }
+ catch (Exception ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogWarning("Wiki '{WikiIdentifier}' not found", wikiIdentifier);
+ return null;
+ }
+ }
+
+ public async Task GetWikiPageAsync(
+ string wikiIdentifier,
+ string path,
+ bool includeContent = true,
+ string? version = null,
+ string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ var projectName = project ?? _options.DefaultProject;
+
+ _logger.LogInformation("Getting wiki page: {Path} from wiki: {WikiIdentifier}", path, wikiIdentifier);
+
+ try
+ {
+ var versionDescriptor = !string.IsNullOrWhiteSpace(version)
+ ? new GitVersionDescriptor { Version = version, VersionType = GitVersionType.Branch }
+ : null;
+
+ var page = await _wikiClient.GetPageAsync(
+ project: projectName,
+ wikiIdentifier: wikiIdentifier,
+ path: path,
+ recursionLevel: VersionControlRecursionType.None,
+ versionDescriptor: versionDescriptor,
+ includeContent: includeContent,
+ cancellationToken: cancellationToken);
+
+ return MapToWikiPageDto(page.Page, page.Page.Content);
+ }
+ catch (Exception ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogWarning("Wiki page '{Path}' not found in wiki '{WikiIdentifier}'", path, wikiIdentifier);
+ return null;
+ }
+ }
+
+ public async Task GetWikiPageTreeAsync(
+ string wikiIdentifier,
+ string path = "/",
+ string recursionLevel = "OneLevel",
+ string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ var projectName = project ?? _options.DefaultProject;
+
+ _logger.LogInformation("Getting wiki page tree: {Path} from wiki: {WikiIdentifier} with recursion: {Recursion}", path, wikiIdentifier, recursionLevel);
+
+ try
+ {
+ var recursion = recursionLevel.Equals("Full", StringComparison.OrdinalIgnoreCase)
+ ? VersionControlRecursionType.Full
+ : VersionControlRecursionType.OneLevel;
+
+ var page = await _wikiClient.GetPageAsync(
+ project: projectName,
+ wikiIdentifier: wikiIdentifier,
+ path: path,
+ recursionLevel: recursion,
+ includeContent: false,
+ cancellationToken: cancellationToken);
+
+ return MapToWikiPageDtoWithSubPages(page.Page);
+ }
+ catch (Exception ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogWarning("Wiki page '{Path}' not found in wiki '{WikiIdentifier}'", path, wikiIdentifier);
+ return null;
+ }
+ }
+
+ private static WikiDto MapToWikiDto(WikiV2 wiki)
+ {
+ return new WikiDto
+ {
+ Id = wiki.Id.ToString(),
+ Name = wiki.Name,
+ Type = wiki.Type.ToString(),
+ Url = wiki.Url,
+ RemoteUrl = wiki.RemoteUrl,
+ ProjectId = wiki.ProjectId.ToString(),
+ RepositoryId = wiki.RepositoryId.ToString(),
+ MappedPath = wiki.MappedPath,
+ Versions = wiki.Versions?.Select(v => v.Version).ToList()
+ };
+ }
+
+ private static WikiPageDto MapToWikiPageDto(WikiPage page, string? content = null)
+ {
+ return new WikiPageDto
+ {
+ Id = page.Id,
+ Path = page.Path,
+ Content = content ?? page.Content,
+ Order = page.Order,
+ GitItemPath = page.GitItemPath,
+ RemoteUrl = page.RemoteUrl,
+ IsParentPage = page.IsParentPage
+ };
+ }
+
+ private static WikiPageDto MapToWikiPageDtoWithSubPages(WikiPage page)
+ {
+ return new WikiPageDto
+ {
+ Id = page.Id,
+ Path = page.Path,
+ Order = page.Order,
+ GitItemPath = page.GitItemPath,
+ RemoteUrl = page.RemoteUrl,
+ IsParentPage = page.IsParentPage,
+ SubPages = page.SubPages?.Select(sp => new WikiPageSummaryDto
+ {
+ Id = sp.Id,
+ Path = sp.Path,
+ Order = sp.Order,
+ GitItemPath = sp.GitItemPath,
+ RemoteUrl = sp.RemoteUrl,
+ IsParentPage = sp.IsParentPage
+ }).ToList()
+ };
+ }
+
+ #endregion
+
public void Dispose()
{
if (_disposed) return;
@@ -1635,6 +1806,7 @@ public void Dispose()
_witClient.Dispose();
_gitClient.Dispose();
_buildClient.Dispose();
+ _wikiClient.Dispose();
_connection.Dispose();
_disposed = true;
}
diff --git a/src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs b/src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs
index 2ef9353..d366658 100644
--- a/src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs
+++ b/src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs
@@ -418,4 +418,59 @@ Task> GetBuildTimelineAsync(
CancellationToken cancellationToken = default);
#endregion
+
+ #region Wiki Operations
+
+ ///
+ /// Gets all wikis in a project.
+ ///
+ /// The project name (optional if default project is configured).
+ /// Cancellation token.
+ /// List of wikis.
+ Task> GetWikisAsync(string? project = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets a specific wiki by its identifier (name or ID).
+ ///
+ /// The wiki name or ID.
+ /// The project name (optional if default project is configured).
+ /// Cancellation token.
+ /// The wiki details.
+ Task GetWikiAsync(string wikiIdentifier, string? project = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets a wiki page by path, including its content.
+ ///
+ /// The wiki name or ID.
+ /// The page path (e.g., "/Home", "/Getting-Started/Installation").
+ /// Whether to include the page content (Markdown).
+ /// Optional version/branch of the wiki.
+ /// The project name (optional if default project is configured).
+ /// Cancellation token.
+ /// The wiki page details.
+ Task GetWikiPageAsync(
+ string wikiIdentifier,
+ string path,
+ bool includeContent = true,
+ string? version = null,
+ string? project = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the sub-pages of a wiki page (page tree).
+ ///
+ /// The wiki name or ID.
+ /// The parent page path (default is root "/").
+ /// How deep to recurse: "OneLevel" or "Full".
+ /// The project name (optional if default project is configured).
+ /// Cancellation token.
+ /// The wiki page with its sub-pages.
+ Task GetWikiPageTreeAsync(
+ string wikiIdentifier,
+ string path = "/",
+ string recursionLevel = "OneLevel",
+ string? project = null,
+ CancellationToken cancellationToken = default);
+
+ #endregion
}
diff --git a/src/Viamus.Azure.Devops.Mcp.Server/Tools/WikiTools.cs b/src/Viamus.Azure.Devops.Mcp.Server/Tools/WikiTools.cs
new file mode 100644
index 0000000..f494cb5
--- /dev/null
+++ b/src/Viamus.Azure.Devops.Mcp.Server/Tools/WikiTools.cs
@@ -0,0 +1,122 @@
+using System.ComponentModel;
+using System.Text.Json;
+using ModelContextProtocol.Server;
+using Viamus.Azure.Devops.Mcp.Server.Services;
+
+namespace Viamus.Azure.Devops.Mcp.Server.Tools;
+
+///
+/// MCP tools for Azure DevOps Wiki operations.
+///
+[McpServerToolType]
+public sealed class WikiTools
+{
+ private readonly IAzureDevOpsService _azureDevOpsService;
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ public WikiTools(IAzureDevOpsService azureDevOpsService)
+ {
+ _azureDevOpsService = azureDevOpsService;
+ }
+
+ [McpServerTool(Name = "get_wikis")]
+ [Description("Gets all wikis in an Azure DevOps project. Returns wiki details including name, type (projectWiki or codeWiki), repository, and versions.")]
+ public async Task GetWikis(
+ [Description("The project name (optional if default project is configured)")] string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ var wikis = await _azureDevOpsService.GetWikisAsync(project, cancellationToken);
+ return JsonSerializer.Serialize(new { count = wikis.Count, wikis }, JsonOptions);
+ }
+
+ [McpServerTool(Name = "get_wiki")]
+ [Description("Gets details of a specific wiki by name or ID. Returns wiki information including type, repository, mapped path, and versions.")]
+ public async Task GetWiki(
+ [Description("The wiki name or ID")] string wikiIdentifier,
+ [Description("The project name (optional if default project is configured)")] string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(wikiIdentifier))
+ {
+ return JsonSerializer.Serialize(new { error = "Wiki name or ID is required" }, JsonOptions);
+ }
+
+ var wiki = await _azureDevOpsService.GetWikiAsync(wikiIdentifier, project, cancellationToken);
+
+ if (wiki is null)
+ {
+ return JsonSerializer.Serialize(new { error = $"Wiki '{wikiIdentifier}' not found" }, JsonOptions);
+ }
+
+ return JsonSerializer.Serialize(wiki, JsonOptions);
+ }
+
+ [McpServerTool(Name = "get_wiki_page")]
+ [Description("Gets a wiki page by path, including its Markdown content. Use this to read the content of a specific wiki page.")]
+ public async Task GetWikiPage(
+ [Description("The wiki name or ID")] string wikiIdentifier,
+ [Description("The page path (e.g., '/Home', '/Getting-Started/Installation')")] string path,
+ [Description("Whether to include the page Markdown content (default true)")] bool includeContent = true,
+ [Description("Optional version/branch of the wiki")] string? version = null,
+ [Description("The project name (optional if default project is configured)")] string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(wikiIdentifier))
+ {
+ return JsonSerializer.Serialize(new { error = "Wiki name or ID is required" }, JsonOptions);
+ }
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return JsonSerializer.Serialize(new { error = "Page path is required" }, JsonOptions);
+ }
+
+ var page = await _azureDevOpsService.GetWikiPageAsync(wikiIdentifier, path, includeContent, version, project, cancellationToken);
+
+ if (page is null)
+ {
+ return JsonSerializer.Serialize(new { error = $"Wiki page '{path}' not found in wiki '{wikiIdentifier}'" }, JsonOptions);
+ }
+
+ return JsonSerializer.Serialize(new
+ {
+ wiki = wikiIdentifier,
+ page
+ }, JsonOptions);
+ }
+
+ [McpServerTool(Name = "get_wiki_page_tree")]
+ [Description("Gets the page hierarchy (tree structure) of a wiki. Returns the sub-pages of a given page path. Useful for browsing wiki structure and discovering available pages.")]
+ public async Task GetWikiPageTree(
+ [Description("The wiki name or ID")] string wikiIdentifier,
+ [Description("The parent page path to browse (default is root '/')")] string path = "/",
+ [Description("Recursion level: 'OneLevel' (immediate children) or 'Full' (all descendants). Default is 'OneLevel'.")] string recursionLevel = "OneLevel",
+ [Description("The project name (optional if default project is configured)")] string? project = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(wikiIdentifier))
+ {
+ return JsonSerializer.Serialize(new { error = "Wiki name or ID is required" }, JsonOptions);
+ }
+
+ var pageTree = await _azureDevOpsService.GetWikiPageTreeAsync(wikiIdentifier, path, recursionLevel, project, cancellationToken);
+
+ if (pageTree is null)
+ {
+ return JsonSerializer.Serialize(new { error = $"Wiki page '{path}' not found in wiki '{wikiIdentifier}'" }, JsonOptions);
+ }
+
+ return JsonSerializer.Serialize(new
+ {
+ wiki = wikiIdentifier,
+ path,
+ recursionLevel,
+ subPageCount = pageTree.SubPages?.Count ?? 0,
+ page = pageTree
+ }, JsonOptions);
+ }
+}