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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.5.1 — 2026-10-15

- An icon: the program, the installer, Settings → Apps, the Start menu and the Claude Desktop extension.
- Start menu folder "ADO Lens": settings (`adolens setup`), status, skills for Claude Desktop, uninstall.
- The installer puts the program folder on the user's PATH: `adolens` works in any new terminal.
- Uninstall asks whether to remove the stored PATs and the settings and cache too (default: keep them); it also takes
the program off the PATH. `adolens logout --all` removes every stored PAT.

## 0.5.0 — 2026-10-15

Tests, templates, and two more ways to install.
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<img src="packaging/icon/icon.png" alt="ADO Lens" width="96" align="right">

# ADO Lens for Azure DevOps — an MCP server for Claude

[![CI](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml/badge.svg)](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml)
Expand Down Expand Up @@ -47,8 +49,12 @@ The install package is not part of the repository (`dist\` is excluded by `.giti
administrator rights. It asks for the Azure DevOps link and the PAT (checked before anything is installed), who uses
it (Product Owner / developer / everything), installs the skills and registers ADO Lens in Claude; an update skips the
questions. Until the program is signed, Windows SmartScreen may show "Windows protected your PC" for a file downloaded
in a browser: *More info → Run anyway* (or use the one-line install above, which avoids it). Uninstall it from
*Settings → Apps*.
in a browser: *More info → Run anyway* (or use the one-line install above, which avoids it).

After installing, the Start menu has an **ADO Lens** folder: *settings* (mode, environments, another connection —
`adolens setup` in a console), *status* (is everything working) and the skills for Claude Desktop; `adolens` works in
any new terminal. Uninstall from *Settings → Apps → ADO Lens* (or the Start menu): it removes the program, its Claude
registrations and skills, and asks whether to remove the stored PATs and settings too (by default they stay).

**Claude Desktop only (extension).** `ado-lens-X.Y.Z.mcpb` from **Releases**: open it with Claude Desktop (or drag it
into *Settings → Extensions*) and fill in the project link and the PAT, which Claude Desktop keeps in the system's secure
Expand Down Expand Up @@ -103,7 +109,7 @@ mentions it once per session; say "update ADO Lens" and `ado_upgrade` downloads
installer in its own window. From a terminal: `adolens upgrade` shows what is new, `adolens upgrade --install` installs
it (`--pre` includes test releases). Settings, PATs and metadata are kept; Claude may stay open.

Uninstall: `uninstall.ps1` in the installation folder (removes the Claude registrations and skills of this installation,
Uninstall (ZIP and one-line installs): `uninstall.ps1` in the installation folder (removes the Claude registrations and skills of this installation,
asks about the stored PATs and the local data).

### The PAT
Expand Down
10 changes: 8 additions & 2 deletions README.pl.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<img src="packaging/icon/icon.png" alt="ADO Lens" width="96" align="right">

# ADO Lens for Azure DevOps — serwer MCP dla Claude'a

[![CI](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml/badge.svg)](https://github.com/willowdev-code/ado-lens/actions/workflows/ci.yml)
Expand Down Expand Up @@ -48,7 +50,11 @@ administratora. Pyta o link do Azure DevOps i PAT (sprawdzane, zanim cokolwiek s
Owner / programista / wszystko), instaluje skille i rejestruje ADO Lens w Claudzie; aktualizacja pomija pytania. Dopóki
program nie jest podpisany, Windows SmartScreen może przy pliku pobranym przeglądarką pokazać „System Windows ochronił
ten komputer”: *Więcej informacji → Uruchom mimo to* (albo użyj instalacji jedną linijką powyżej, która tego unika).
Odinstalowanie: *Ustawienia → Aplikacje*.

Po instalacji w menu Start jest folder **ADO Lens**: *ustawienia* (tryb, środowiska, kolejne połączenie — `adolens setup`
w konsoli), *status* (czy wszystko działa) i skille dla Claude Desktop; `adolens` działa w każdym nowym terminalu.
Odinstalowanie: *Ustawienia → Aplikacje → ADO Lens* (albo z menu Start) — usuwa program, jego rejestracje w Claudzie
i skille, i pyta, czy usunąć też zapisane PAT-y i ustawienia (domyślnie zostają).

**Tylko Claude Desktop (rozszerzenie).** `ado-lens-X.Y.Z.mcpb` z **Releases**: otwórz go w Claude Desktop (albo
przeciągnij do *Settings → Extensions*) i wpisz link do projektu oraz PAT, który Claude Desktop trzyma w bezpiecznym
Expand Down Expand Up @@ -105,7 +111,7 @@ w sesji; wystarczy powiedzieć „zaktualizuj ADO Lens”, a `ado_upgrade` pobie
instalator w osobnym oknie. Z terminala: `adolens upgrade` pokazuje, co nowego, `adolens upgrade --install` instaluje
(`--pre` uwzględnia wydania testowe). Ustawienia, PAT-y i metadane zostają; Claude może być otwarty.

Deinstalacja: `uninstall.ps1` w folderze instalacji (usuwa rejestracje i skille tej instalacji w Claude, pyta o zapisane
Deinstalacja (instalacja z ZIP-a i jedną linijką): `uninstall.ps1` w folderze instalacji (usuwa rejestracje i skille tej instalacji w Claude, pyta o zapisane
PAT-y i dane lokalne).

### PAT
Expand Down
1 change: 1 addition & 0 deletions pack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ if (-not $NoZip) {
[IO.File]::WriteAllText((Join-Path $mcpbStage 'manifest.json'), $manifest, (New-Object Text.UTF8Encoding $false))
Copy-Item (Join-Path $stage 'bin') $mcpbStage -Recurse
Copy-Item (Join-Path $stage 'LICENSE') $mcpbStage
Copy-Item (Join-Path $PSScriptRoot 'packaging\icon\icon.png') $mcpbStage
$mcpb = Join-Path $OutDir "ado-lens-$version.mcpb"
New-Zip $mcpbStage $mcpb $false
Write-Checksum $mcpb 'mcpb'
Expand Down
Binary file added packaging/icon/adolens.ico
Binary file not shown.
Binary file added packaging/icon/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 91 additions & 0 deletions packaging/icon/make-icon.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<#
.SYNOPSIS
Draws the ADO Lens icon: a lens over three work item bars on an Azure blue tile.
Writes adolens.ico (16-256 px, PNG frames) and icon.png (512 px) next to this script.
#>
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing

function New-RoundedRect([float] $x, [float] $y, [float] $w, [float] $h, [float] $r) {
$p = New-Object System.Drawing.Drawing2D.GraphicsPath
$d = 2 * $r
$p.AddArc($x, $y, $d, $d, 180, 90)
$p.AddArc($x + $w - $d, $y, $d, $d, 270, 90)
$p.AddArc($x + $w - $d, $y + $h - $d, $d, $d, 0, 90)
$p.AddArc($x, $y + $h - $d, $d, $d, 90, 90)
$p.CloseFigure()
return $p
}

function New-IconBitmap([int] $size) {
$bmp = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$g.Clear([System.Drawing.Color]::Transparent)
$s = [float] $size

# Tile: Azure blue, lighter at the top.
$margin = [Math]::Max(0.5, $s * 0.03)
$tile = New-RoundedRect $margin $margin ($s - 2 * $margin) ($s - 2 * $margin) ($s * 0.2)
$grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush (New-Object System.Drawing.PointF 0, 0), (New-Object System.Drawing.PointF 0, $s),
([System.Drawing.Color]::FromArgb(255, 0, 137, 230)), ([System.Drawing.Color]::FromArgb(255, 0, 90, 170))
$g.FillPath($grad, $tile)

$white = [System.Drawing.Color]::White
# Lens: a ring, centred a little up and left, with a handle to the bottom right.
$cx = $s * 0.43; $cy = $s * 0.43; $r = $s * 0.24
$ring = [Math]::Max(1.5, $s * 0.075)
if ($size -ge 32) {
# Three work item bars inside the lens (left out at small sizes, where they would blur).
$bar = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(235, 255, 255, 255))
$bh = $s * 0.045
foreach ($i in 0..2) {
$w = $r * (1.05 - 0.25 * $i)
$y = $cy - $r * 0.42 + $i * $r * 0.42 - $bh / 2
$path = New-RoundedRect ($cx - $r * 0.55) $y $w $bh ($bh / 2)
$g.FillPath($bar, $path)
}
}
$pen = New-Object System.Drawing.Pen $white, $ring
$g.DrawEllipse($pen, $cx - $r, $cy - $r, 2 * $r, 2 * $r)
$handle = New-Object System.Drawing.Pen $white, ([Math]::Max(2, $s * 0.11))
$handle.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$handle.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$a = 0.7071 * ($r + $ring / 2)
$g.DrawLine($handle, $cx + $a, $cy + $a, $s * 0.76, $s * 0.76)
$g.Dispose()
return $bmp
}

$dir = $PSScriptRoot
$sizes = 16, 20, 24, 32, 40, 48, 64, 128, 256
$frames = foreach ($n in $sizes) {
$bmp = New-IconBitmap $n
$ms = New-Object System.IO.MemoryStream
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
, $ms.ToArray()
}

# ICO: header, one directory entry per size, then the PNG frames.
$out = New-Object System.IO.MemoryStream
$w = New-Object System.IO.BinaryWriter $out
$w.Write([UInt16]0); $w.Write([UInt16]1); $w.Write([UInt16]$sizes.Count)
$offset = 6 + 16 * $sizes.Count
for ($i = 0; $i -lt $sizes.Count; $i++) {
$n = $sizes[$i]
$dim = if ($n -ge 256) { 0 } else { $n }
$w.Write([byte]$dim); $w.Write([byte]$dim); $w.Write([byte]0); $w.Write([byte]0)
$w.Write([UInt16]1); $w.Write([UInt16]32)
$w.Write([UInt32]$frames[$i].Length); $w.Write([UInt32]$offset)
$offset += $frames[$i].Length
}
foreach ($f in $frames) { $w.Write($f) }
$w.Flush()
[IO.File]::WriteAllBytes((Join-Path $dir 'adolens.ico'), $out.ToArray())

$big = New-IconBitmap 512
$big.Save((Join-Path $dir 'icon.png'), [System.Drawing.Imaging.ImageFormat]::Png)
$big.Dispose()
Write-Host "written adolens.ico and icon.png in $dir"
83 changes: 83 additions & 0 deletions packaging/inno/ado-lens.iss
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ AppSupportURL=https://github.com/willowdev-code/ado-lens/issues
DefaultDirName={localappdata}\Programs\ADO Lens
DisableDirPage=yes
DisableProgramGroupPage=yes
DefaultGroupName=ADO Lens
DisableReadyPage=yes
SetupIconFile={#SourcePath}..\icon\adolens.ico
; The program folder goes on the user's PATH, so "adolens" works in any new terminal.
ChangesEnvironment=yes
PrivilegesRequired=lowest
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
Expand All @@ -52,6 +56,19 @@ Source: "{#DesktopSkills}\*.zip"; DestDir: "{app}\skills-for-claude-desktop"; Fl
; Claude Code reads skills from %USERPROFILE%\.claude\skills (only when Claude Code is there); removed on uninstall.
Source: "{#Source}\skills\*"; DestDir: "{%USERPROFILE}\.claude\skills"; Flags: recursesubdirs ignoreversion; Check: HasClaudeCode

[Icons]
; Start menu: settings and status open in a console that stays open (adolens commands work there too).
Name: "{group}\{cm:IconSettings}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\adolens.exe"" setup"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\adolens.exe"
Name: "{group}\{cm:IconStatus}"; Filename: "{cmd}"; Parameters: "/k """"{app}\bin\adolens.exe"" status"""; WorkingDir: "{app}"; IconFilename: "{app}\bin\adolens.exe"
Name: "{group}\{cm:IconSkills}"; Filename: "{app}\skills-for-claude-desktop"
Name: "{group}\{cm:UninstallProgram,ADO Lens}"; Filename: "{uninstallexe}"; IconFilename: "{app}\bin\adolens.exe"

[Registry]
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin'))

[UninstallDelete]
Type: files; Name: "{app}\bin\*.old"

[UninstallRun]
Filename: "{app}\bin\adolens.exe"; Parameters: "unregister"; Flags: runhidden waituntilterminated; RunOnceId: "unregister"

Expand All @@ -60,6 +77,14 @@ Filename: "{app}\skills-for-claude-desktop"; Description: "{cm:OpenSkills}"; Fla
Filename: "https://github.com/willowdev-code/ado-lens#readme"; Description: "{cm:OpenReadme}"; Flags: postinstall shellexec skipifsilent unchecked

[CustomMessages]
en.IconSettings=ADO Lens - settings
en.IconStatus=ADO Lens - status
en.IconSkills=ADO Lens - skills for Claude Desktop
en.RemoveData=Also remove the stored personal access tokens (Windows Credential Manager) and the settings and cache of ADO Lens?%n%nChoose No to keep them for a later installation.
pl.IconSettings=ADO Lens - ustawienia
pl.IconStatus=ADO Lens - status
pl.IconSkills=ADO Lens - skille dla Claude Desktop
pl.RemoveData=Usunąć też zapisane tokeny dostępu (Menedżer poświadczeń Windows) oraz ustawienia i pamięć podręczną ADO Lens?%n%nWybierz Nie, aby zachować je na potrzeby ponownej instalacji.
en.ConnTitle=Azure DevOps
en.ConnSub=Where your work items are
en.ConnText=Paste a link to your Azure DevOps project from the browser (for example https://dev.azure.com/contoso/Sales), or the organization name.
Expand Down Expand Up @@ -119,6 +144,64 @@ var
function SetEnvironmentVariable(lpName: String; lpValue: String): Boolean;
external 'SetEnvironmentVariableW@kernel32.dll stdcall';

{ Whether a folder is missing from the user's PATH (compared without case and trailing backslash). }
function NeedsAddPath(Dir: String): Boolean;
var
Path: String;
begin
if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) then
begin
Result := True;
Exit;
end;
Result := Pos(';' + Lowercase(RemoveBackslashUnlessRoot(Dir)) + ';', ';' + Lowercase(Path) + ';') = 0;
end;

procedure RemoveFromPath(Dir: String);
var
Path, Item, Kept: String;
P: Integer;
begin
if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) then Exit;
Kept := '';
Path := Path + ';';
while Path <> '' do
begin
P := Pos(';', Path);
Item := Copy(Path, 1, P - 1);
Delete(Path, 1, P);
if (Item <> '') and (Lowercase(RemoveBackslashUnlessRoot(Item)) <> Lowercase(RemoveBackslashUnlessRoot(Dir))) then
begin
if Kept <> '' then Kept := Kept + ';';
Kept := Kept + Item;
end;
end;
RegWriteExpandStringValue(HKCU, 'Environment', 'Path', Kept);
end;

var
RemoveUserData: Boolean;

{ Before the files go: ask whether to remove the stored PATs and the local data (default: keep them), and sign out
with the program while it is still there. After: take the program folder off the PATH and delete the data. }
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
Code: Integer;
begin
if CurUninstallStep = usUninstall then
begin
RemoveUserData := not UninstallSilent and
(MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES);
if RemoveUserData then
Exec(ExpandConstant('{app}\bin\adolens.exe'), 'logout --all', '', SW_HIDE, ewWaitUntilTerminated, Code);
end
else if CurUninstallStep = usPostUninstall then
begin
RemoveFromPath(ExpandConstant('{app}\bin'));
if RemoveUserData then DelTree(ExpandConstant('{localappdata}\ado-lens'), True, True, True);
end;
end;

function HasClaudeCode: Boolean;
begin
Result := DirExists(ExpandConstant('{%USERPROFILE}\.claude'));
Expand Down
1 change: 1 addition & 0 deletions packaging/mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"documentation": "https://github.com/willowdev-code/ado-lens#readme",
"support": "https://github.com/willowdev-code/ado-lens/issues",
"license": "MIT",
"icon": "icon.png",
"keywords": ["azure devops", "work items", "sprint", "wiki", "tfvc", "git", "pull requests"],
"server": {
"type": "binary",
Expand Down
4 changes: 3 additions & 1 deletion src/AdoLens/AdoLens.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<!-- Drawn by packaging/icon/make-icon.ps1; shown in Explorer, Settings -> Apps and the Start menu. -->
<ApplicationIcon>..\..\packaging\icon\adolens.ico</ApplicationIcon>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -21,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.5.0</Version>
<Version>0.5.1</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 8 additions & 1 deletion src/AdoLens/Cli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static void PrintUsage() => Console.WriteLine("""
login add or update a connection: organization, default project and PAT
--org <name|url> --project <name> --team <name> --connection <name>
--pat-stdin (read the PAT from standard input) --default (make it the default connection)
logout remove the stored PAT of a connection --connection <name>
logout remove the stored PAT of a connection --connection <name> | --all
connections list the connections
config show or change settings
--connection <name> --project <name> --team <name> (defaults of a connection)
Expand Down Expand Up @@ -184,6 +184,13 @@ public async Task<int> LoginAsync()
public int Logout()
{
var settings = Settings.Load(SettingsPath);
if (cl.Flag("all"))
{
// Uninstall: every stored PAT of the connections in the settings.
foreach (var conn in settings.Connections.Keys)
Console.WriteLine(CredentialStore.Delete(conn) ? $"removed {CredentialStore.TargetFor(conn)}" : $"no stored PAT for '{conn}'");
return 0;
}
var name = cl.Opt("connection") ?? settings.DefaultConnection;
if (string.IsNullOrWhiteSpace(name)) { Console.WriteLine("no connection configured - nothing to remove"); return 0; }
Console.WriteLine(CredentialStore.Delete(name)
Expand Down
2 changes: 1 addition & 1 deletion src/AdoLens/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed partial class CommandLine
["login"] = ["connection=", "org=", "project=", "team=", "pat-stdin", "pat-env=", "default"],
["tests"] = ["connection=", "team=", "detail="],
["test-create"] = ["connection=", "file=", "plan=", "suite="],
["logout"] = ["connection="],
["logout"] = ["connection=", "all"],
["connections"] = [],
["trace"] = ["connection=", "detail="],
["pr"] = ["connection=", "project=", "repo=", "list=", "detail="],
Expand Down