MCP Server Memory File
by g0t4
- src
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { promises as fs } from "node:fs";
import { always_log, verbose_log } from "./logs.js";
// TODO add configurable path to this file (will fix pathing issues too)
// FYI pathing is to workaround no support for a cwd in claude_desktop_config.json (yet?)
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
let memories_file_path = __dirname + "/memories.txt";
verbose_log("INFO: memories file path", memories_file_path);
export async function readMemories(): Promise<string> {
// if the file doesn't exist, treat that as NO memories
const file_exists = await fs
.access(memories_file_path, fs.constants.F_OK)
.then(() => true) // if does exist, set to true
.catch(() => false); // error callback only invoked if does not exist
if (!file_exists) {
// dont wanna log failures when the file is just not there
return "";
}
return (await fs.readFile(memories_file_path, "utf8")) ?? "";
}
export async function appendMemory(memory: string): Promise<CallToolResult> {
try {
memory = memory.trim(); // trim trailing/leading whitespace (notably newlines)
// TODO great case for a test case :)
await fs.appendFile(memories_file_path, "\n" + memory); // FYI append will create or append
return {
isError: false,
content: [],
};
} catch (error) {
// TODO test by locking file and try to write (macOS) - or use readonly dir
return errorResult("appendMemory", error);
}
}
export async function listMemory(): Promise<CallToolResult> {
try {
return {
isError: false,
content: [
{
// FYI spec for tool results: https://spec.modelcontextprotocol.io/specification/server/tools/#tool-result
// clearly is only text, image or resource
type: "text",
text: await readMemories(),
name: "memories",
},
],
};
} catch (error) {
return errorResult("listMemory", error);
}
}
function errorResult(what: string, error: any) {
// TODO do I really want to return details here? or just log those?
const message = error instanceof Error ? error.message : String(error);
const response: CallToolResult = {
isError: true,
content: [
{
type: "text",
text: message,
name: "error",
},
],
};
always_log(`WARN: ${what} failed`, response);
return response;
}
export async function deleteMemory(query: string): Promise<CallToolResult> {
query = query.trim(); // trim trailing/leading whitespace (notably newlines)
try {
const memories = await readMemories();
const lines = memories.split("\n");
// TODO config formatter to knock it off adding () around single param lamdas
const keep_memories = lines.filter((l) => !l.includes(query));
await fs.writeFile(memories_file_path, keep_memories.join("\n"));
return {
isError: false,
content: [],
};
} catch (error) {
return errorResult("deleteMemory", error);
}
}
export async function searchMemory(query: string): Promise<CallToolResult> {
query = query.trim(); // trim trailing/leading whitespace (notably newlines)
try {
const memories = await readMemories();
const lines = memories.split("\n");
const keep_memories = lines.filter((l) => l.includes(query));
return {
isError: false,
content: [
{
type: "text",
text: keep_memories.join("\n"),
name: "memories",
},
],
};
} catch (error) {
return errorResult("searchMemory", error);
}
}