Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.7.3 — 2026-10-16

Fixes from a real question: "pull requests merged into staging after release X, with their work items and tags".

- `ado_pr` and `ado_pipeline` (reading: pull request lists and details, merged PRs, builds, deployments, artifacts) are
in every mode now, the Product Owner one too; creating and completing PRs and running pipelines stay for developers.
- `ado_changes` of a pull request, changeset or commit lists its work items with type, state and tags.
- `ado_trace` checks the release stages in parallel and skips deployments older than the change: it no longer runs
out of time on items with several pull requests.
- `ado_file path=/ repo=<r> history=true version=<branch>`: the history of a whole repository or folder on a branch,
with pull requests and work items. An environment name given as the branch ("sbx03") is turned into its branch.
- The environment map reads "UAT (sbx03) = branch staging" instead of "UAT (sbx03) [staging]".
- The server instructions name the tools for "what went into a branch or an environment".

## 0.7.2 — 2026-10-16

- `ado_pr merged=<branch>` (`adolens pr --merged staging --since 7d`): pull requests completed into a branch, newest
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ never a permission.
|---|---|:-:|:-:|:-:|
| `core` | `ado_status`, `ado_settings`, `ado_upgrade`, `ado_fields` | ✓ | ✓ | ✓ |
| `work` | `ado_work_item`, `ado_query`, `ado_create`, `ado_update`, `ado_comment`, `ado_link`, `ado_search`, `ado_undo`, `ado_attachments`, `ado_create_tree` | ✓ | ✓ | ✓ |
| `pr` | `ado_pr`, `ado_pr_update` | – | ✓ | ✓ |
| `pipelines` | `ado_pipeline`, `ado_pipeline_run` | – | ✓ | ✓ |
| `pr` | `ado_pr_update` | – | ✓ | ✓ |
| `pipelines` | `ado_pipeline_run` | – | ✓ | ✓ |
| `boards` | `ado_sprint`, `ado_board`, `ado_backlog`, `ado_my_day`, `ado_health`, `ado_release_notes`, `ado_metrics` | ✓ | – | ✓ |
| `wiki` | `ado_wiki`, `ado_wiki_write` | ✓ | ✓ | ✓ |
| `trace` | `ado_trace`, `ado_changes`, `ado_file`, `ado_label`, `ado_review` | ✓ | ✓ | ✓ |
| `trace` | `ado_trace`, `ado_changes`, `ado_file`, `ado_label`, `ado_review`, `ado_pr`, `ado_pipeline` | ✓ | ✓ | ✓ |
| `recipes` | `ado_recipe` | ✓ | ✓ | ✓ |
| `test` | `ado_tests`, `ado_test_create` | ✓ | – | ✓ |
| `test`, `recipes` | planned | ✓ | (recipes) | ✓ |
Expand Down
6 changes: 3 additions & 3 deletions README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ Nigdy nie jest uprawnieniem.
|---|---|:-:|:-:|:-:|
| `core` | `ado_status`, `ado_settings`, `ado_upgrade`, `ado_fields` | ✓ | ✓ | ✓ |
| `work` | `ado_work_item`, `ado_query`, `ado_create`, `ado_update`, `ado_comment`, `ado_link`, `ado_search`, `ado_undo`, `ado_attachments`, `ado_create_tree` | ✓ | ✓ | ✓ |
| `pr` | `ado_pr`, `ado_pr_update` | – | ✓ | ✓ |
| `pipelines` | `ado_pipeline`, `ado_pipeline_run` | – | ✓ | ✓ |
| `pr` | `ado_pr_update` | – | ✓ | ✓ |
| `pipelines` | `ado_pipeline_run` | – | ✓ | ✓ |
| `boards` | `ado_sprint`, `ado_board`, `ado_backlog`, `ado_my_day`, `ado_health`, `ado_release_notes`, `ado_metrics` | ✓ | – | ✓ |
| `wiki` | `ado_wiki`, `ado_wiki_write` | ✓ | ✓ | ✓ |
| `trace` | `ado_trace`, `ado_changes`, `ado_file`, `ado_label`, `ado_review` | ✓ | ✓ | ✓ |
| `trace` | `ado_trace`, `ado_changes`, `ado_file`, `ado_label`, `ado_review`, `ado_pr`, `ado_pipeline` | ✓ | ✓ | ✓ |
| `recipes` | `ado_recipe` | ✓ | ✓ | ✓ |
| `test` | `ado_tests`, `ado_test_create` | ✓ | – | ✓ |
| `test`, `recipes` | w planie | ✓ | (recipes) | ✓ |
Expand Down
2 changes: 1 addition & 1 deletion src/AdoLens/AdoLens.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<AssemblyTitle>ADO Lens for Azure DevOps</AssemblyTitle>
<Description>Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP</Description>
<Copyright>Copyright © 2026 WillowDev</Copyright>
<Version>0.7.2</Version>
<Version>0.7.3</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
17 changes: 17 additions & 0 deletions src/AdoLens/Knowledge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ sealed record FileRef(bool Tfvc, string Path, string? RepoId, string? RepoName,
{
path = path.Trim().Replace('\\', '/');
if (path.StartsWith("$/", StringComparison.Ordinal)) return (new FileRef(true, path, null, null, project), null);
// The whole repository ("/") or a folder ("src/"): no file search, the repository is needed.
if (path == "/" || path.EndsWith('/'))
{
if (repo is not null)
{
var r = await session.Code.RepoAsync(repo, project, ct);
return (new FileRef(false, "/" + path.Trim('/'), r.Id, r.Name, r.Project ?? project), null);
}
var all = await session.Client.GetListAsync($"{AdoClient.Scope(project)}_apis/git/repositories", ct);
if (all.Count == 1) return (new FileRef(false, "/" + path.Trim('/'), Json.Str(all[0], "id"), Json.Str(all[0], "name"), project), null);
return (null, $"{path} is a folder: give repo=<name> ({string.Join(", ", all.Select(x => Json.Str(x, "name")).Take(30))})");
}
if (repo is not null && path.Contains('/'))
{
var r = await session.Code.RepoAsync(repo, project, ct);
Expand Down Expand Up @@ -190,7 +202,12 @@ public static async Task<string> FileAsync(Lens lens, string? connection, string
var name = session.Project(project);
var (file, choices) = await ResolveAsync(session, name, path, repo, ct);
if (file is null) return choices!;
// An environment name ("sbx03", "UAT") instead of a branch: the branch of the environment map.
if (version is not null && session.Config.Environments.FirstOrDefault(e => !e.Key.Contains('/') &&
(e.Value.Equals(version, StringComparison.OrdinalIgnoreCase) || e.Value.Contains(version, StringComparison.OrdinalIgnoreCase))) is { Key: { } branch })
version = branch;
if (history) return await HistoryAsync(session, file, version, top, ct);
if (path.Trim() == "/" || path.TrimEnd().EndsWith('/')) return $"{path} is a folder: history=true lists its changes";

string? text;
string where;
Expand Down
21 changes: 17 additions & 4 deletions src/AdoLens/McpTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public static class McpTools
""",
[Toolsets.Trace] = """
- Checking work against what was asked: ado_trace, then read the diffs with ado_changes - never assume.
- What went into a branch or an environment: ado_pr merged=<branch> since=<time> (with work items and tags); the time of a
deployment: ado_pipeline deployments=true; a branch's whole history: ado_file path=/ repo=<r> history=true version=<branch>.
""",
[Toolsets.Pr] = """
- PR description: what and why with the work item, changes as bullets by area, how to check, risks only if any.
Expand Down Expand Up @@ -317,8 +319,9 @@ public static Task<string> ReadFile(Lens lens, string path,
() => Knowledge.FileAsync(lens, connection, project, path, repo, version, part, history, top, detail, ct));
}

[McpServerToolType, Toolset(Toolsets.Pr)]
public static class PrTools
// Reading pull requests and builds is part of checking work (PO mode too); changing them is for developers.
[McpServerToolType, Toolset(Toolsets.Trace)]
public static class PrReadTools
{
[McpServerTool(Name = "ado_pr", ReadOnly = true, Idempotent = true)]
[Description("Pull requests: without id the ones waiting for my review and mine (list=active: all active); merged=<branch>: those completed into a branch (since, top), with their work items; with id: status, reviewers and votes, checks, work items, open comment threads.")]
Expand All @@ -338,6 +341,11 @@ public static Task<string> Pr(Lens lens,
: merged is not null ? PullRequestQueries.MergedAsync(lens, connection, project, merged, repo, since, top, ct)
: PullRequestQueries.ListAsync(lens, connection, project, list, ct));

}

[McpServerToolType, Toolset(Toolsets.Pr)]
public static class PrTools
{
[McpServerTool(Name = "ado_pr_update")]
[Description("Acts on pull requests: create (repo, source, target, title, text, workItems, reviewers, draft), comment (text, optional file and line), reply (thread, text), resolve (thread, status), vote (approve|suggestions|wait|reject|reset), publish, complete, abandon. @[name] in text notifies.")]
public static Task<string> PrUpdate(Lens lens,
Expand Down Expand Up @@ -369,8 +377,8 @@ public static Task<string> PrUpdate(Lens lens,
}, ct));
}

[McpServerToolType, Toolset(Toolsets.Pipelines)]
public static class PipelineTools
[McpServerToolType, Toolset(Toolsets.Trace)]
public static class PipelineReadTools
{
[McpServerTool(Name = "ado_pipeline", ReadOnly = true, Idempotent = true)]
[Description("Builds: recent runs (optionally of one pipeline or branch); with build: why it failed - failed steps, errors and the error lines of the log; artifacts=true lists its artifacts, artifact=<name> its files (part=<file> reads one, save=true saves it to disk); deployments=true: recent release deployments (definition, stage) with dates.")]
Expand All @@ -396,6 +404,11 @@ public static Task<string> Pipeline(Lens lens,
: PipelineQueries.BuildAsync(lens, connection, project, b, detail, ct)
: PipelineQueries.ListAsync(lens, connection, project, definition, branch, top, ct));

}

[McpServerToolType, Toolset(Toolsets.Pipelines)]
public static class PipelineTools
{
[McpServerTool(Name = "ado_pipeline_run")]
[Description("Queues a run of a pipeline, optionally on a branch.")]
public static Task<string> Run(Lens lens,
Expand Down
10 changes: 7 additions & 3 deletions src/AdoLens/Queries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static string SettingsAnswer(Lens lens, string? connection, string? envir
var sb = new StringBuilder(environments is null ? "" : "saved.\n");
sb.AppendLine($"connection {cfg.Connection}: https://dev.azure.com/{conn.Organization}, project {conn.Project ?? "-"}, team {conn.Team ?? "-"}, mode {lens.Settings.Mode}");
sb.Append(conn.Environments.Count == 0 ? "local environments: not set"
: "environments (in flow order): " + string.Join(" -> ", conn.Environments.Select(e => $"{e.Value} [{e.Key}]")));
: "environments (in flow order): " + string.Join(" -> ", conn.Environments.Select(e => Flow(e.Key, e.Value))));
return sb.ToString();
}

Expand All @@ -85,7 +85,7 @@ public static async Task<string> SettingsAsync(Lens lens, string? connection, st
var map = ParseEnvironments(environments);
var url = await session.Team.SaveAsync(name, map, ct);
return $"saved for the whole team on the wiki page \"{TeamSettings.PagePath}\" of {name}: " +
string.Join(" -> ", map.Select(e => $"{e.Value} [{e.Key}]")) + $"\n{url}";
string.Join(" -> ", map.Select(e => Flow(e.Key, e.Value))) + $"\n{url}";
}
var text = SettingsAnswer(lens, connection, environments);
if (environments is not null) return text;
Expand All @@ -94,7 +94,7 @@ public static async Task<string> SettingsAsync(Lens lens, string? connection, st
var p = s.Project(project);
var (effective, source) = await EnvironmentFlow.EffectiveAsync(s, p, ct);
if (effective.Count > 0)
return text + $"\nin use for {p} ({source}): " + string.Join(" -> ", effective.Select(e => $"{e.Value} [{e.Key}]"));
return text + $"\nin use for {p} ({source}): " + string.Join(" -> ", effective.Select(e => Flow(e.Key, e.Value)));
var detected = await EnvironmentFlow.DetectAsync(s, p, ct);
return text + (detected.Count == 0
? $"\nno release stages or long-lived branches found in {p}"
Expand All @@ -103,6 +103,10 @@ public static async Task<string> SettingsAsync(Lens lens, string? connection, st
"(scope=team for everyone, after the user agrees).");
}

/// <summary>"UAT (sbx03) = branch staging", "UAT + SCM = release stage Main-Deploy/Stage 1": the name, then what it is.</summary>
internal static string Flow(string key, string name) =>
$"{name} = {(key.Contains('/') ? "release stage" : "branch")} {key}";

static Dictionary<string, string> ParseEnvironments(string environments)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Expand Down
Loading