Add support for official dotMemory CLI via DOTMEMORY_PATH#41
Add support for official dotMemory CLI via DOTMEMORY_PATH#41godofphonk wants to merge 7 commits intoMarcelRoozekrans:mainfrom
Conversation
- Replace hard dependency on dotnet-dotmemory global tool - Add DotMemoryCommand record for flexible command resolution - Support 4 modes: DOTMEMORY_PATH env var, PATH discovery, local tool, global tool - Update SnapshotManager to use ResolveCommandAsync instead of hardcoded command - Add dotMemory, dotMemory.exe, dotMemory.sh to ProcessFilter exclusions - Create FakeDotMemoryToolManager for testing - Update all tests to use FakeDotMemoryToolManager This allows memorylens-mcp to work with official JetBrains dotMemory CLI installed via Toolbox or zip, not just the non-existent NuGet global tool.
There was a problem hiding this comment.
Pull request overview
Adds robust discovery for the JetBrains dotMemory CLI (including explicit DOTMEMORY_PATH) and updates snapshot execution paths and tests to use the resolved command instead of a hardcoded dotnet-dotmemory.
Changes:
- Reworked
DotMemoryToolManagerto resolve dotMemory via env var, PATH, local tool manifest, or global tool. - Updated
SnapshotManagerto execute dotMemory using the resolved command and provide a clearer “not found” error. - Updated process exclusions and adjusted tests to pass a tool manager into
SnapshotManager.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/MemoryLens.Mcp.Tests/Profiler/SnapshotManagerTests.cs | Injects a tool manager into SnapshotManager tests and updates error expectation text. |
| tests/MemoryLens.Mcp.Tests/Profiler/SnapshotManagerCompareTests.cs | Injects a tool manager into comparison tests. |
| tests/MemoryLens.Mcp.Tests/Profiler/FakeDotMemoryToolManager.cs | Adds a test double intended to control dotMemory command resolution. |
| tests/MemoryLens.Mcp.Tests/Integration/ToolIntegrationTests.cs | Updates integration tests to construct SnapshotManager with a tool manager. |
| src/MemoryLens.Mcp/Profiler/SnapshotManager.cs | Uses ResolveCommandAsync() and executes the resolved dotMemory command. |
| src/MemoryLens.Mcp/Profiler/ProcessFilter.cs | Adds dotMemory executables/scripts to the excluded process list. |
| src/MemoryLens.Mcp/Profiler/DotMemoryToolManager.cs | Implements multi-mode dotMemory discovery and legacy fallback installation logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/MemoryLens.Mcp.Tests/Profiler/FakeDotMemoryToolManager.cs
Outdated
Show resolved
Hide resolved
- Make ResolveCommandAsync virtual for proper override in tests - Re-throw OperationCanceledException in TryRunAsync for proper cancellation handling - Restore global tool update logic in EnsureInstalledAsync for 'up-to-date' contract - Add exit code validation in TryProbeAsync to prevent false positives
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Closing this PR after investigation. I found that JetBrains.dotMemory.Console.linux-x64 and JetBrains.dotMemory.Console.windows-x64 packages exist in NuGet, but they are not .NET tools and cannot be installed via Therefore, my original patch with DOTMEMORY_PATH support remains the correct approach for users with official JetBrains dotMemory CLI installations from Toolbox or zip downloads. The patch allows memorylens-mcp to work with official dotMemory CLI without requiring a non-existent global tool. I'll keep the fork for personal use with DOTMEMORY_PATH pointing to my JetBrains Toolbox installation. |
- Add samples/DotMemoryPathTest console application - Include README.md with usage instructions and explanations - Update PR description with comprehensive documentation - Demonstrate how to use DOTMEMORY_PATH with official JetBrains dotMemory CLI
- Fix EnsureInstalledAsync to only run update when using global tool shim - Add caching to ResolveCommandAsync for performance optimization - Add InvalidateCache method to refresh cached command - Add tests for DOTMEMORY_PATH precedence - Add tests for ProcessFilter new exclusions (dotMemory, dotMemory.exe, dotMemory.sh)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/MemoryLens.Mcp.Tests/Profiler/DotMemoryToolManagerTests.cs
Outdated
Show resolved
Hide resolved
tests/MemoryLens.Mcp.Tests/Profiler/DotMemoryToolManagerTests.cs
Outdated
Show resolved
Hide resolved
tests/MemoryLens.Mcp.Tests/Profiler/DotMemoryToolManagerTests.cs
Outdated
Show resolved
Hide resolved
- Revert server.json to original state (remove personal paths) - Fix ProjectReference path in sample - Rename test_dotmemory.csproj to DotMemoryPathTest.csproj - Improve tests for DOTMEMORY_PATH with real temp file - Improve InvalidateCache test to verify actual cache invalidation - EnsureInstalledAsync now uses CommandKind instead of heuristics - Add --version validation for DOTMEMORY_PATH without separators
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // For non-path candidates (commands without separators), validate with --version | ||
| if (!LooksLikePath(candidate)) | ||
| { | ||
| var version = TryProbeSync(candidate); | ||
| if (version is null) | ||
| continue; | ||
| } | ||
|
|
||
| return new DotMemoryCommand( | ||
| candidate, | ||
| "", | ||
| $"{variableName} ({candidate})", | ||
| null, |
There was a problem hiding this comment.
When DOTMEMORY_PATH/MEMORYLENS_DOTMEMORY_PATH points to an existing file, it is accepted without validating that it is actually the dotMemory CLI. This can cause EnsureInstalledAsync() to report the tool as available even if the path points to an unrelated executable/file, and later snapshot/compare calls will fail in a confusing way. Consider probing the configured path with --version (using the existing async runner/cancellation) and only accepting it when the probe succeeds (exit code 0).
| // For non-path candidates (commands without separators), validate with --version | |
| if (!LooksLikePath(candidate)) | |
| { | |
| var version = TryProbeSync(candidate); | |
| if (version is null) | |
| continue; | |
| } | |
| return new DotMemoryCommand( | |
| candidate, | |
| "", | |
| $"{variableName} ({candidate})", | |
| null, | |
| var version = TryProbeSync(candidate); | |
| if (version is null) | |
| continue; | |
| return new DotMemoryCommand( | |
| candidate, | |
| "", | |
| $"{variableName} ({candidate})", | |
| version, |
| "put dotMemory in PATH, or install dotnet-dotmemory. " + | ||
| installResult.Error); |
There was a problem hiding this comment.
This failure message concatenates the install error without a separator, which can make it hard to read (and may produce a trailing space when Error is empty). Consider adding a clear delimiter (e.g., ": " or a newline) and guarding against null/whitespace so the user-facing message stays consistent.
| "put dotMemory in PATH, or install dotnet-dotmemory. " + | |
| installResult.Error); | |
| "put dotMemory in PATH, or install dotnet-dotmemory." + | |
| (string.IsNullOrWhiteSpace(installResult.Error) | |
| ? string.Empty | |
| : $" Details: {installResult.Error}")); |
| false, | ||
| null, | ||
| null, | ||
| "dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH."); |
There was a problem hiding this comment.
The 'dotMemory CLI not found' guidance only mentions ensure_dotmemory and DOTMEMORY_PATH, but resolution also supports MEMORYLENS_DOTMEMORY_PATH and PATH discovery. Updating this message to reflect the supported discovery modes will reduce user confusion when PATH discovery would work or when the alternative env var is used.
| "dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH."); | |
| "dotMemory CLI not found. Run ensure_dotmemory, set DOTMEMORY_PATH or MEMORYLENS_DOTMEMORY_PATH, or ensure dotMemory is available on PATH."); |
| null, | ||
| null, | ||
| 0, | ||
| "dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH."); |
There was a problem hiding this comment.
Same as above: the error text only mentions ensure_dotmemory/DOTMEMORY_PATH even though command resolution also supports MEMORYLENS_DOTMEMORY_PATH and PATH discovery. Aligning this message with DotMemoryToolManager’s supported modes will make troubleshooting easier.
| "dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH."); | |
| "dotMemory CLI not found. Run ensure_dotmemory, set DOTMEMORY_PATH or MEMORYLENS_DOTMEMORY_PATH, or make dotMemory available on PATH."); |
|
|
||
| try | ||
| { | ||
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", "/custom/path/first-dotMemory.sh"); | ||
|
|
||
| var firstCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | ||
| Assert.NotNull(firstCommand); | ||
| Assert.Contains("first-dotMemory.sh", firstCommand.FileName); | ||
|
|
||
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", "/custom/path/second-dotMemory.sh"); | ||
| manager.InvalidateCache(); | ||
|
|
||
| var secondCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | ||
| Assert.NotNull(secondCommand); | ||
| Assert.Contains("second-dotMemory.sh", secondCommand.FileName); | ||
| Assert.NotEqual(firstCommand.FileName, secondCommand.FileName); | ||
| } | ||
| finally | ||
| { | ||
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", null); |
There was a problem hiding this comment.
This test sets DOTMEMORY_PATH to absolute paths that don’t exist. DotMemoryToolManager.ResolveConfiguredPath() ignores path-like values when File.Exists() is false, so ResolveCommandAsync() will fall through to PATH/local/global probing and firstCommand.FileName is unlikely to contain first-dotMemory.sh (making the test fail or become environment-dependent). To make this deterministic, create temporary files for the configured paths (or use a non-path value and mock the probe) so the configured-path branch is actually exercised.
| try | |
| { | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", "/custom/path/first-dotMemory.sh"); | |
| var firstCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | |
| Assert.NotNull(firstCommand); | |
| Assert.Contains("first-dotMemory.sh", firstCommand.FileName); | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", "/custom/path/second-dotMemory.sh"); | |
| manager.InvalidateCache(); | |
| var secondCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | |
| Assert.NotNull(secondCommand); | |
| Assert.Contains("second-dotMemory.sh", secondCommand.FileName); | |
| Assert.NotEqual(firstCommand.FileName, secondCommand.FileName); | |
| } | |
| finally | |
| { | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", null); | |
| var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); | |
| var firstConfiguredPath = Path.Combine(tempDirectory, "first-dotMemory.sh"); | |
| var secondConfiguredPath = Path.Combine(tempDirectory, "second-dotMemory.sh"); | |
| Directory.CreateDirectory(tempDirectory); | |
| File.WriteAllText(firstConfiguredPath, string.Empty); | |
| File.WriteAllText(secondConfiguredPath, string.Empty); | |
| try | |
| { | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", firstConfiguredPath); | |
| var firstCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | |
| Assert.NotNull(firstCommand); | |
| Assert.Equal(firstConfiguredPath, firstCommand.FileName); | |
| Assert.Equal(DotMemoryCommandKind.ExplicitPath, firstCommand.Kind); | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", secondConfiguredPath); | |
| manager.InvalidateCache(); | |
| var secondCommand = await manager.ResolveCommandAsync(TestContext.Current.CancellationToken); | |
| Assert.NotNull(secondCommand); | |
| Assert.Equal(secondConfiguredPath, secondCommand.FileName); | |
| Assert.Equal(DotMemoryCommandKind.ExplicitPath, secondCommand.Kind); | |
| Assert.NotEqual(firstCommand.FileName, secondCommand.FileName); | |
| } | |
| finally | |
| { | |
| Environment.SetEnvironmentVariable("DOTMEMORY_PATH", null); | |
| if (File.Exists(firstConfiguredPath)) | |
| { | |
| File.Delete(firstConfiguredPath); | |
| } | |
| if (File.Exists(secondConfiguredPath)) | |
| { | |
| File.Delete(secondConfiguredPath); | |
| } | |
| if (Directory.Exists(tempDirectory)) | |
| { | |
| Directory.Delete(tempDirectory); | |
| } |
- Add --version validation for DOTMEMORY_PATH with existing files - Improve error message formatting with separator and null check - Update error messages to mention all discovery modes (DOTMEMORY_PATH, MEMORYLENS_DOTMEMORY_PATH, PATH) - Fix InvalidateCache test to use real temp files instead of non-existent paths
- Accept DOTMEMORY_PATH even if --version validation fails - User explicitly configured the path, so trust it - Fixes issue where dotmemory command fails --version in some environments
Failing test fixThe CI failure in Fix for [Fact]
public async Task EnsureDotMemory_ReturnsInstalledMessage()
{
var savedPath = Environment.GetEnvironmentVariable("DOTMEMORY_PATH");
Environment.SetEnvironmentVariable("DOTMEMORY_PATH", null);
try
{
var runner = new FakeProcessRunner(
exitCode: 0,
output: "dotnet-dotmemory 2024.3.5 dotnet-dotmemory");
var manager = new DotMemoryToolManager(runner);
var tool = new EnsureDotMemoryTool(manager);
var result = await tool.ensure_dotmemory(TestContext.Current.CancellationToken);
Assert.Contains("2024.3.5", result);
}
finally
{
Environment.SetEnvironmentVariable("DOTMEMORY_PATH", savedPath);
}
}Same pattern should be applied to any other integration tests that use a real |
|
Thank you for this contribution and for identifying the root problem — the original We've incorporated your changes (with a small test isolation fix for the In a follow-up PR we'll also bundle the official |
Summary
Add support for official JetBrains dotMemory CLI via DOTMEMORY_PATH environment variable, removing hard dependency on the non-existent
dotnet-dotmemoryglobal tool from NuGet.Problem
The current implementation requires
dotnet-dotmemoryas a global .NET tool installed from NuGet feeds. However, JetBrains does not distribute dotMemory CLI through public NuGet feeds as a .NET tool.Note: JetBrains.dotMemory.Console.linux-x64 and JetBrains.dotMemory.Console.windows-x64 packages exist in NuGet, but they are regular NuGet packages, not .NET tools. They cannot be installed via
dotnet tool install -gand cannot be used as global tools. They are redistributable console applications that require manual installation or download from JetBrains.This makes memorylens-mcp unusable for users with official JetBrains dotMemory CLI installations from:
JetBrains.dotMemory.Console.*Solution
Modified
DotMemoryToolManagerto support 4 discovery modes for dotMemory CLI:DOTMEMORY_PATHorMEMORYLENS_DOTMEMORY_PATHenvironment variabledotMemory.sh/dotMemory(Linux) ordotMemory.exe(Windows)dotnet tool run dotnet-dotmemorydotnet-dotmemoryglobal toolChanges
ResolveCommandAsync()instead of hardcoded"dotnet-dotmemory"dotMemory,dotMemory.exe,dotMemory.shFakeDotMemoryToolManagerand updated all testsResolveCommandAsyncvirtual, re-throwOperationCanceledException, restore global tool update, validate exit codessamples/DotMemoryPathTestdemonstrating DOTMEMORY_PATH usageUsage
Setting DOTMEMORY_PATH
Finding dotMemory CLI Path
JetBrains Toolbox (Linux):
JetBrains Toolbox (Windows):
Testing with Sample
Expected output:
Using with memorylens-mcp
After setting DOTMEMORY_PATH, the
ensure_dotmemoryMCP tool will automatically detect and use the dotMemory CLI specified in the environment variable without attempting NuGet installation.Backward Compatibility
The old global tool scenario remains as a fallback, so existing configurations continue to work.