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
5 changes: 5 additions & 0 deletions .changeset/memory-file-path-base-dir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/server-memory": patch
---

Internal: `ensureMemoryFilePath` accepts the directory its default files live in, so the server's tests no longer write into the package directory. No change to how the server chooses or migrates its memory file.
71 changes: 41 additions & 30 deletions src/memory/__tests__/file-path.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
// Tests for how the memory server picks its graph file: MEMORY_FILE_PATH
// handling, the memory.json -> memory.jsonl migration, and "~" expansion.
//
// Every test runs ensureMemoryFilePath against its own temporary directory.
// Importing ../index.js starts the server's main(), which runs the same
// migration on the package directory, and Vitest runs test files in parallel:
// files written beside the source are shared with every other test file, and
// were migrated away mid-test (#4922).
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import { fileURLToPath } from "url";
import {
ensureMemoryFilePath,
defaultMemoryPath,
expandHome,
} from "../index.js";

describe("ensureMemoryFilePath", () => {
const testDir = path.dirname(fileURLToPath(import.meta.url));
const oldMemoryPath = path.join(testDir, "..", "memory.json");
const newMemoryPath = path.join(testDir, "..", "memory.jsonl");

let originalEnv: string | undefined;
let testDir: string;
let oldMemoryPath: string;
let newMemoryPath: string;

beforeEach(() => {
beforeEach(async () => {
// Save original environment variable
originalEnv = process.env.MEMORY_FILE_PATH;
// Delete environment variable
delete process.env.MEMORY_FILE_PATH;

testDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-memory-file-path-"));
oldMemoryPath = path.join(testDir, "memory.json");
newMemoryPath = path.join(testDir, "memory.jsonl");
});

afterEach(async () => {
Expand All @@ -31,25 +41,15 @@ describe("ensureMemoryFilePath", () => {
delete process.env.MEMORY_FILE_PATH;
}

// Clean up test files
try {
await fs.unlink(oldMemoryPath);
} catch {
// Ignore if file doesn't exist
}
try {
await fs.unlink(newMemoryPath);
} catch {
// Ignore if file doesn't exist
}
await fs.rm(testDir, { recursive: true, force: true });
});

describe("with MEMORY_FILE_PATH environment variable", () => {
it("should return absolute path when MEMORY_FILE_PATH is absolute", async () => {
const absolutePath = "/tmp/custom-memory.jsonl";
const absolutePath = path.join(testDir, "custom-memory.jsonl");
process.env.MEMORY_FILE_PATH = absolutePath;

const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(absolutePath);
});
Expand All @@ -58,17 +58,28 @@ describe("ensureMemoryFilePath", () => {
const relativePath = "custom-memory.jsonl";
process.env.MEMORY_FILE_PATH = relativePath;

const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(path.join(testDir, relativePath));
});

it("should resolve a relative path against the package directory by default", async () => {
const relativePath = "custom-memory.jsonl";
process.env.MEMORY_FILE_PATH = relativePath;

// No base directory: the call the server makes. Touches no file.
const result = await ensureMemoryFilePath();

expect(path.isAbsolute(result)).toBe(true);
expect(result).toContain("custom-memory.jsonl");
expect(result).toBe(
path.join(path.dirname(defaultMemoryPath), relativePath),
);
});

it("should handle Windows absolute paths", async () => {
const windowsPath = "C:\\temp\\memory.jsonl";
process.env.MEMORY_FILE_PATH = windowsPath;

const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

// On Windows, should return as-is; on Unix, will be treated as relative
if (process.platform === "win32") {
Expand All @@ -81,7 +92,7 @@ describe("ensureMemoryFilePath", () => {
it('should expand a leading "~/" to the home directory', async () => {
process.env.MEMORY_FILE_PATH = "~/custom-memory.jsonl";

const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(path.join(os.homedir(), "custom-memory.jsonl"));
expect(path.isAbsolute(result)).toBe(true);
Expand All @@ -90,9 +101,9 @@ describe("ensureMemoryFilePath", () => {

describe("without MEMORY_FILE_PATH environment variable", () => {
it("should return default path when no files exist", async () => {
const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(defaultMemoryPath);
expect(result).toBe(newMemoryPath);
});

it("should migrate from memory.json to memory.jsonl when only old file exists", async () => {
Expand All @@ -103,9 +114,9 @@ describe("ensureMemoryFilePath", () => {
.spyOn(console, "error")
.mockImplementation(() => {});

const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(defaultMemoryPath);
expect(result).toBe(newMemoryPath);

// Verify migration happened
const newFileExists = await fs
Expand Down Expand Up @@ -140,9 +151,9 @@ describe("ensureMemoryFilePath", () => {
.spyOn(console, "error")
.mockImplementation(() => {});

const result = await ensureMemoryFilePath();
const result = await ensureMemoryFilePath(testDir);

expect(result).toBe(defaultMemoryPath);
expect(result).toBe(newMemoryPath);

// Verify no migration happened (both files should still exist)
const newFileExists = await fs
Expand All @@ -167,7 +178,7 @@ describe("ensureMemoryFilePath", () => {
const testContent = '{"entities": [{"name": "test", "type": "person"}]}';
await fs.writeFile(oldMemoryPath, testContent);

await ensureMemoryFilePath();
await ensureMemoryFilePath(testDir);

const migratedContent = await fs.readFile(newMemoryPath, "utf-8");
expect(migratedContent).toBe(testContent);
Expand Down
27 changes: 15 additions & 12 deletions src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import { randomBytes } from "crypto";
import { fileURLToPath } from "url";
import { SERVER_VERSION } from "./version.js";

// The package directory: where the default graph file lives, and what a
// relative MEMORY_FILE_PATH resolves against.
const defaultMemoryDir = path.dirname(fileURLToPath(import.meta.url));

// Define memory file path using environment variable with fallback
export const defaultMemoryPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"memory.jsonl",
);
export const defaultMemoryPath = path.join(defaultMemoryDir, "memory.jsonl");

// Expand a leading "~" to the user's home directory. MCP clients pass
// MEMORY_FILE_PATH from JSON config, where no shell performs tilde expansion,
Expand All @@ -32,23 +33,25 @@ export function expandHome(filepath: string): string {
return filepath;
}

// Handle backward compatibility: migrate memory.json to memory.jsonl if needed
export async function ensureMemoryFilePath(): Promise<string> {
// Handle backward compatibility: migrate memory.json to memory.jsonl if needed.
// baseDir is the directory the default files live in. The server never passes
// it; it exists so tests can run the migration in a temporary directory rather
// than in the package directory, which every importer of this module shares.
export async function ensureMemoryFilePath(
baseDir: string = defaultMemoryDir,
): Promise<string> {
Comment thread
Copilot marked this conversation as resolved.
if (process.env.MEMORY_FILE_PATH) {
// Custom path provided. Expand a leading "~" first, then resolve relative
// paths against the package directory (absolute paths are used as-is).
const customPath = expandHome(process.env.MEMORY_FILE_PATH);
return path.isAbsolute(customPath)
? customPath
: path.join(path.dirname(fileURLToPath(import.meta.url)), customPath);
: path.join(baseDir, customPath);
}

// No custom path set, check for backward compatibility migration
const oldMemoryPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"memory.json",
);
const newMemoryPath = defaultMemoryPath;
const oldMemoryPath = path.join(baseDir, "memory.json");
const newMemoryPath = path.join(baseDir, "memory.jsonl");

try {
// Check if old file exists and new file doesn't
Expand Down
Loading