Skip to content

Add support for official dotMemory CLI via DOTMEMORY_PATH#41

Closed
godofphonk wants to merge 7 commits intoMarcelRoozekrans:mainfrom
godofphonk:main
Closed

Add support for official dotMemory CLI via DOTMEMORY_PATH#41
godofphonk wants to merge 7 commits intoMarcelRoozekrans:mainfrom
godofphonk:main

Conversation

@godofphonk
Copy link
Copy Markdown
Contributor

@godofphonk godofphonk commented Apr 11, 2026

Summary

Add support for official JetBrains dotMemory CLI via DOTMEMORY_PATH environment variable, removing hard dependency on the non-existent dotnet-dotmemory global tool from NuGet.

Problem

The current implementation requires dotnet-dotmemory as 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 -g and 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 Toolbox
  • Direct download from jetbrains.com
  • Platform-specific NuGet packages JetBrains.dotMemory.Console.*

Solution

Modified DotMemoryToolManager to support 4 discovery modes for dotMemory CLI:

  1. Explicit path via DOTMEMORY_PATH or MEMORYLENS_DOTMEMORY_PATH environment variable
  2. PATH discovery - searches for dotMemory.sh/dotMemory (Linux) or dotMemory.exe (Windows)
  3. Local tool manifest - supports dotnet tool run dotnet-dotmemory
  4. Global tool - legacy fallback for dotnet-dotmemory global tool

Changes

  • DotMemoryToolManager.cs: Complete rewrite with flexible command resolution
  • SnapshotManager.cs: Updated to use ResolveCommandAsync() instead of hardcoded "dotnet-dotmemory"
  • ProcessFilter.cs: Added exclusions for dotMemory, dotMemory.exe, dotMemory.sh
  • Tests: Created FakeDotMemoryToolManager and updated all tests
  • Copilot review: Made ResolveCommandAsync virtual, re-throw OperationCanceledException, restore global tool update, validate exit codes
  • Sample: Added samples/DotMemoryPathTest demonstrating DOTMEMORY_PATH usage

Usage

Setting DOTMEMORY_PATH

# Linux/macOS
export DOTMEMORY_PATH="/path/to/dotMemory.sh"

# Windows
set DOTMEMORY_PATH=C:\path\to\dotMemory.exe

Finding dotMemory CLI Path

JetBrains Toolbox (Linux):

ls ~/.local/share/JetBrains/Toolbox/apps/rider/tools/profiler/
# Look for dotMemory or dotMemory.sh

JetBrains Toolbox (Windows):

dir %LOCALAPPDATA%\JetBrains\Toolbox\apps\rider\tools\profiler\
# Look for dotMemory.exe

Testing with Sample

# Set DOTMEMORY_PATH first
export DOTMEMORY_PATH="/home/gospodin/.local/share/JetBrains/Toolbox/apps/rider/tools/profiler/dotMemory.sh"

# Run the sample test
dotnet run --project samples/DotMemoryPathTest/DotMemoryPathTest.csproj

Expected output:

Testing EnsureInstalledAsync with DOTMEMORY_PATH...
IsInstalled: True
Message: DOTMEMORY_PATH (/path/to/dotMemory.sh) is available.

✓ SUCCESS: dotMemory CLI is available via DOTMEMORY_PATH

Using with memorylens-mcp

After setting DOTMEMORY_PATH, the ensure_dotmemory MCP 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.

- 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.
Copilot AI review requested due to automatic review settings April 11, 2026 23:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DotMemoryToolManager to resolve dotMemory via env var, PATH, local tool manifest, or global tool.
  • Updated SnapshotManager to 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.

- 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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@godofphonk
Copy link
Copy Markdown
Contributor Author

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 dotnet tool install -g. They are regular NuGet packages.

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.

@godofphonk godofphonk closed this Apr 11, 2026
@godofphonk godofphonk reopened this Apr 11, 2026
- 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)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

- 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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +147 to +159
// 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,
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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,

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +80
"put dotMemory in PATH, or install dotnet-dotmemory. " +
installResult.Error);
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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}"));

Copilot uses AI. Check for mistakes.
false,
null,
null,
"dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH.");
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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.");

Copilot uses AI. Check for mistakes.
null,
null,
0,
"dotMemory CLI not found. Run ensure_dotmemory first or set DOTMEMORY_PATH.");
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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.");

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +82

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);
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
- 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
@MarcelRoozekrans
Copy link
Copy Markdown
Owner

Failing test fix

The CI failure in EnsureDotMemory_ReturnsInstalledMessage is caused by env var leakage from ResolveCommand_Prefers_DOTMEMORY_Path_Over_Other_Modes. Both tests run in parallel — when DOTMEMORY_PATH is still set to a temp file, DotMemoryToolManager resolves it via ExplicitPath before the fake process runner gets a chance to return the version string.

Fix for ToolIntegrationTests.cs:

[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 DotMemoryToolManager and don't expect DOTMEMORY_PATH to be set.

@MarcelRoozekrans
Copy link
Copy Markdown
Owner

Thank you for this contribution and for identifying the root problem — the original dotnet-dotmemory global tool approach was never going to work since JetBrains doesn't publish it as a proper NuGet tool.

We've incorporated your changes (with a small test isolation fix for the DOTMEMORY_PATH env var leaking between parallel tests) in #42.

In a follow-up PR we'll also bundle the official JetBrains.dotMemory.Console redistributable NuGet packages directly, so dotMemory works out of the box with zero user setup. Your DOTMEMORY_PATH discovery will remain as an override for users who want to point at their own installation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants