diff --git a/packages/memory/package.json b/packages/memory/package.json index b33f835..99a1583 100644 --- a/packages/memory/package.json +++ b/packages/memory/package.json @@ -1,6 +1,6 @@ { "name": "@arvoretech/memory-mcp", - "version": "1.1.1", + "version": "1.2.0", "description": "MCP server for team memory — persistent knowledge base with semantic search for AI-assisted development", "main": "dist/index.js", "type": "module", diff --git a/packages/memory/src/embeddings.ts b/packages/memory/src/embeddings.ts index 67c4bdf..a34951d 100644 --- a/packages/memory/src/embeddings.ts +++ b/packages/memory/src/embeddings.ts @@ -13,7 +13,10 @@ export class EmbeddingEngine { async init(): Promise { try { - const { pipeline } = await import("@xenova/transformers"); + const { pipeline, env } = await import("@xenova/transformers"); + if (process.env.TRANSFORMERS_CACHE_DIR) { + env.cacheDir = process.env.TRANSFORMERS_CACHE_DIR; + } this.pipeline = (await pipeline( "feature-extraction", this.modelName diff --git a/packages/memory/src/server.ts b/packages/memory/src/server.ts index 53564fe..bce5ed9 100644 --- a/packages/memory/src/server.ts +++ b/packages/memory/src/server.ts @@ -119,10 +119,15 @@ export class MemoryMCPServer { } async start(): Promise { - await this.store.load(); const transport = new StdioServerTransport(); await this.server.connect(transport); - console.error("Memory MCP Server started successfully"); + console.error("Memory MCP Server connected, loading store in background..."); + + this.store.load().then(() => { + console.error("Memory MCP Server store loaded successfully"); + }).catch((error) => { + console.error(`Failed to load store: ${error instanceof Error ? error.message : error}`); + }); } setupGracefulShutdown(): void { diff --git a/packages/memory/src/store.test.ts b/packages/memory/src/store.test.ts index 7f8ea64..7ba0650 100644 --- a/packages/memory/src/store.test.ts +++ b/packages/memory/src/store.test.ts @@ -64,7 +64,7 @@ describe("MemoryStore", () => { describe("load", () => { it("should load an empty directory without errors", async () => { await store.load(); - const result = store.list(); + const result = await store.list(); expect(result).toEqual([]); }); @@ -96,7 +96,7 @@ describe("MemoryStore", () => { ); await store.load(); - const result = store.list(); + const result = await store.list(); expect(result).toHaveLength(2); expect(result.map((m) => m.title)).toContain("Use PostgreSQL"); @@ -119,7 +119,7 @@ describe("MemoryStore", () => { ); await store.load(); - const result = store.list(); + const result = await store.list(); expect(result[0].title).toBe("New"); expect(result[1].title).toBe("Old"); @@ -224,7 +224,7 @@ describe("MemoryStore", () => { ); await store.load(); - const entry = store.get("use-postgres"); + const entry = await store.get("use-postgres"); expect(entry).not.toBeNull(); expect(entry!.title).toBe("Use PostgreSQL"); @@ -233,7 +233,7 @@ describe("MemoryStore", () => { it("should return null for non-existent id", async () => { await store.load(); - const entry = store.get("does-not-exist"); + const entry = await store.get("does-not-exist"); expect(entry).toBeNull(); }); }); @@ -310,10 +310,10 @@ describe("MemoryStore", () => { ); await store.load(); - expect(store.get("glossary-term")).not.toBeNull(); + expect(await store.get("glossary-term")).not.toBeNull(); await store.remove("glossary-term"); - expect(store.get("glossary-term")).toBeNull(); + expect(await store.get("glossary-term")).toBeNull(); }); it("should throw for non-existent memory", async () => { @@ -338,7 +338,7 @@ describe("MemoryStore", () => { ); await store.load(); - const decisions = store.list({ category: "decisions" }); + const decisions = await store.list({ category: "decisions" }); expect(decisions).toHaveLength(1); expect(decisions[0].category).toBe("decisions"); @@ -359,8 +359,8 @@ describe("MemoryStore", () => { ); await store.load(); - const active = store.list({ status: "active" }); - const archived = store.list({ status: "archived" }); + const active = await store.list({ status: "active" }); + const archived = await store.list({ status: "archived" }); expect(active).toHaveLength(1); expect(active[0].title).toBe("Active"); diff --git a/packages/memory/src/store.ts b/packages/memory/src/store.ts index d9b2608..7e7db9b 100644 --- a/packages/memory/src/store.ts +++ b/packages/memory/src/store.ts @@ -36,6 +36,7 @@ export class MemoryStore { private db: lancedb.Connection | null = null; private table: lancedb.Table | null = null; private loaded = false; + private loadingPromise: Promise | null = null; constructor(memoriesPath: string, embeddingModel?: string) { this.memoriesPath = memoriesPath; @@ -43,6 +44,11 @@ export class MemoryStore { } async load(): Promise { + this.loadingPromise = this.doLoad(); + return this.loadingPromise; + } + + private async doLoad(): Promise { this.catalog = []; await this.embeddings.init(); @@ -186,17 +192,20 @@ export class MemoryStore { } } - private ensureLoaded(): void { - if (!this.loaded) { - throw new MemoryMCPError("Store not loaded. Call load() first.", "NOT_LOADED"); + private async ensureLoaded(): Promise { + if (this.loaded) return; + if (this.loadingPromise) { + await this.loadingPromise; + return; } + throw new MemoryMCPError("Store not loaded. Call load() first.", "NOT_LOADED"); } async search( query: string, opts?: { category?: MemoryCategory; status?: MemoryStatus; limit?: number } ): Promise<(MemoryCatalogEntry & { score: number })[]> { - this.ensureLoaded(); + await this.ensureLoaded(); const status = opts?.status || "active"; const limit = opts?.limit || 10; @@ -274,12 +283,12 @@ export class MemoryStore { .map((s) => ({ ...this.toCatalogEntry(s.entry), score: round(s.score) })); } - list(opts?: { + async list(opts?: { category?: MemoryCategory; status?: MemoryStatus; limit?: number; - }): MemoryCatalogEntry[] { - this.ensureLoaded(); + }): Promise { + await this.ensureLoaded(); let filtered = [...this.catalog]; @@ -294,8 +303,8 @@ export class MemoryStore { return filtered.slice(0, limit).map((e) => this.toCatalogEntry(e)); } - get(id: string): MemoryEntry | null { - this.ensureLoaded(); + async get(id: string): Promise { + await this.ensureLoaded(); return this.catalog.find((m) => m.id === id) || null; } @@ -359,7 +368,7 @@ export class MemoryStore { } async archive(id: string): Promise { - const entry = this.get(id); + const entry = await this.get(id); if (!entry) { throw new MemoryMCPError(`Memory "${id}" not found`, "NOT_FOUND"); } @@ -380,7 +389,7 @@ export class MemoryStore { } async remove(id: string): Promise { - const entry = this.get(id); + const entry = await this.get(id); if (!entry) { throw new MemoryMCPError(`Memory "${id}" not found`, "NOT_FOUND"); } diff --git a/packages/memory/src/tools.ts b/packages/memory/src/tools.ts index 3a1d5f2..058c8de 100644 --- a/packages/memory/src/tools.ts +++ b/packages/memory/src/tools.ts @@ -44,7 +44,7 @@ export class MemoryMCPTools { async getMemory(params: GetMemoryParams): Promise { try { - const entry = this.store.get(params.id); + const entry = await this.store.get(params.id); if (!entry) { return { @@ -116,7 +116,7 @@ export class MemoryMCPTools { async listMemories(params: ListMemoriesParams): Promise { try { - const results = this.store.list({ + const results = await this.store.list({ category: params.category, status: params.status, limit: params.limit,