@aeriondyseti/vector-memory-mcp 1.0.2-dev.0 → 1.1.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/package.json +4 -4
  2. package/dist/src/config/index.d.ts +10 -0
  3. package/dist/src/config/index.d.ts.map +1 -1
  4. package/dist/src/config/index.js +13 -0
  5. package/dist/src/config/index.js.map +1 -1
  6. package/dist/src/db/conversation-history.repository.d.ts +24 -0
  7. package/dist/src/db/conversation-history.repository.d.ts.map +1 -0
  8. package/dist/src/db/conversation-history.repository.js +184 -0
  9. package/dist/src/db/conversation-history.repository.js.map +1 -0
  10. package/dist/src/db/conversation-history.schema.d.ts +10 -0
  11. package/dist/src/db/conversation-history.schema.d.ts.map +1 -0
  12. package/dist/src/db/conversation-history.schema.js +31 -0
  13. package/dist/src/db/conversation-history.schema.js.map +1 -0
  14. package/dist/src/db/lancedb-utils.d.ts +35 -0
  15. package/dist/src/db/lancedb-utils.d.ts.map +1 -0
  16. package/dist/src/db/lancedb-utils.js +77 -0
  17. package/dist/src/db/lancedb-utils.js.map +1 -0
  18. package/dist/src/db/memory.repository.d.ts +2 -12
  19. package/dist/src/db/memory.repository.d.ts.map +1 -1
  20. package/dist/src/db/memory.repository.js +13 -56
  21. package/dist/src/db/memory.repository.js.map +1 -1
  22. package/dist/src/db/schema.d.ts +4 -1
  23. package/dist/src/db/schema.d.ts.map +1 -1
  24. package/dist/src/db/schema.js +8 -4
  25. package/dist/src/db/schema.js.map +1 -1
  26. package/dist/src/index.js +8 -0
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/mcp/handlers.d.ts +3 -0
  29. package/dist/src/mcp/handlers.d.ts.map +1 -1
  30. package/dist/src/mcp/handlers.js +149 -17
  31. package/dist/src/mcp/handlers.js.map +1 -1
  32. package/dist/src/mcp/tools.d.ts +3 -0
  33. package/dist/src/mcp/tools.d.ts.map +1 -1
  34. package/dist/src/mcp/tools.js +54 -3
  35. package/dist/src/mcp/tools.js.map +1 -1
  36. package/dist/src/services/conversation-history.service.d.ts +64 -0
  37. package/dist/src/services/conversation-history.service.d.ts.map +1 -0
  38. package/dist/src/services/conversation-history.service.js +244 -0
  39. package/dist/src/services/conversation-history.service.js.map +1 -0
  40. package/dist/src/services/memory.service.d.ts +24 -0
  41. package/dist/src/services/memory.service.d.ts.map +1 -1
  42. package/dist/src/services/memory.service.js +58 -0
  43. package/dist/src/services/memory.service.js.map +1 -1
  44. package/dist/src/services/session-parser.d.ts +59 -0
  45. package/dist/src/services/session-parser.d.ts.map +1 -0
  46. package/dist/src/services/session-parser.js +147 -0
  47. package/dist/src/services/session-parser.js.map +1 -0
  48. package/dist/src/types/conversation-history.d.ts +74 -0
  49. package/dist/src/types/conversation-history.d.ts.map +1 -0
  50. package/dist/src/types/conversation-history.js +2 -0
  51. package/dist/src/types/conversation-history.js.map +1 -0
  52. package/dist/src/types/memory.d.ts +4 -2
  53. package/dist/src/types/memory.d.ts.map +1 -1
  54. package/package.json +4 -4
  55. package/src/config/index.ts +23 -0
  56. package/src/db/conversation-history.repository.ts +255 -0
  57. package/src/db/conversation-history.schema.ts +40 -0
  58. package/src/db/lancedb-utils.ts +97 -0
  59. package/src/db/memory.repository.ts +18 -67
  60. package/src/db/schema.ts +17 -21
  61. package/src/index.ts +16 -0
  62. package/src/mcp/handlers.ts +178 -22
  63. package/src/mcp/tools.ts +66 -3
  64. package/src/services/conversation-history.service.ts +320 -0
  65. package/src/services/memory.service.ts +74 -0
  66. package/src/services/session-parser.ts +232 -0
  67. package/src/types/conversation-history.ts +82 -0
  68. package/src/types/memory.ts +4 -3
@@ -0,0 +1,97 @@
1
+ import * as lancedb from "@lancedb/lancedb";
2
+ import { Index, rerankers, type Table } from "@lancedb/lancedb";
3
+ import type { Schema } from "apache-arrow";
4
+
5
+ /**
6
+ * Escapes a string for use in LanceDB/DataFusion SQL WHERE clauses.
7
+ * Doubles single quotes to prevent SQL injection (standard SQL escaping).
8
+ */
9
+ export function escapeLanceDbString(value: string): string {
10
+ return value.replace(/'/g, "''");
11
+ }
12
+
13
+ /**
14
+ * Converts LanceDB's Arrow Vector type to a plain number[].
15
+ * LanceDB returns an Arrow Vector object which is iterable but not an array.
16
+ */
17
+ export function arrowVectorToArray(value: unknown): number[] {
18
+ return Array.isArray(value)
19
+ ? value
20
+ : (Array.from(value as Iterable<number>) as number[]);
21
+ }
22
+
23
+ /**
24
+ * Opens an existing table or creates it with the given schema.
25
+ * Does NOT cache — callers should cache the returned Table if desired.
26
+ */
27
+ export async function getOrCreateTable(
28
+ db: lancedb.Connection,
29
+ name: string,
30
+ schema: Schema
31
+ ): Promise<Table> {
32
+ const names = await db.tableNames();
33
+ if (names.includes(name)) {
34
+ return await db.openTable(name);
35
+ }
36
+ return await db.createTable(name, [], { schema });
37
+ }
38
+
39
+ /**
40
+ * Creates a mutex-guarded function that ensures an FTS index exists on a table's content column.
41
+ *
42
+ * Once the FTS index is confirmed/created, the promise is retained for the lifetime of the
43
+ * caller — the index persists in LanceDB, so re-checking is unnecessary. On error, the
44
+ * mutex resets so the next call can retry.
45
+ *
46
+ * The key design constraint: the promise must be captured synchronously (before any await)
47
+ * to prevent concurrent callers from racing past the guard.
48
+ */
49
+ export function createFtsMutex(
50
+ getTable: () => Promise<Table>
51
+ ): () => Promise<void> {
52
+ let promise: Promise<void> | null = null;
53
+
54
+ return () => {
55
+ if (promise) return promise;
56
+
57
+ promise = (async () => {
58
+ const table = await getTable();
59
+ const indices = await table.listIndices();
60
+ const hasFtsIndex = indices.some(
61
+ (idx) => idx.columns.includes("content") && idx.indexType === "FTS"
62
+ );
63
+
64
+ if (!hasFtsIndex) {
65
+ await table.createIndex("content", {
66
+ config: Index.fts(),
67
+ });
68
+ await table.waitForIndex(["content_idx"], 30);
69
+ }
70
+ })().catch((error) => {
71
+ promise = null;
72
+ throw error;
73
+ });
74
+
75
+ return promise;
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Creates a promise-mutex for RRFReranker instantiation.
81
+ * Same pattern as createFtsMutex: create once, cache forever, reset on error.
82
+ */
83
+ export function createRerankerMutex(
84
+ k: number = 60
85
+ ): () => Promise<rerankers.RRFReranker> {
86
+ let promise: Promise<rerankers.RRFReranker> | null = null;
87
+
88
+ return () => {
89
+ if (!promise) {
90
+ promise = rerankers.RRFReranker.create(k).catch((e) => {
91
+ promise = null;
92
+ throw e;
93
+ });
94
+ }
95
+ return promise;
96
+ };
97
+ }
@@ -1,6 +1,7 @@
1
1
  import * as lancedb from "@lancedb/lancedb";
2
- import { Index, rerankers, type Table } from "@lancedb/lancedb";
2
+ import { type Table } from "@lancedb/lancedb";
3
3
  import { TABLE_NAME, memorySchema } from "./schema.js";
4
+ import { arrowVectorToArray, createFtsMutex, createRerankerMutex, escapeLanceDbString } from "./lancedb-utils.js";
4
5
  import {
5
6
  type Memory,
6
7
  type HybridRow,
@@ -8,14 +9,18 @@ import {
8
9
  } from "../types/memory.js";
9
10
 
10
11
  export class MemoryRepository {
11
- // Mutex for FTS index creation - ensures only one index creation runs at a time
12
- // Once set, this promise is never cleared (FTS index persists in the database)
13
- private ftsIndexPromise: Promise<void> | null = null;
14
-
15
12
  // Mutex for schema migration - runs once per instance to add missing columns
16
13
  private migrationPromise: Promise<void> | null = null;
17
14
 
18
- constructor(private db: lancedb.Connection) { }
15
+ // FTS index mutex — once created, the promise is never cleared (index persists in LanceDB)
16
+ private ensureFtsIndex: () => Promise<void>;
17
+
18
+ // Cached reranker — k=60 is constant, no need to recreate per search
19
+ private getReranker = createRerankerMutex();
20
+
21
+ constructor(private db: lancedb.Connection) {
22
+ this.ensureFtsIndex = createFtsMutex(() => this.getTable());
23
+ }
19
24
 
20
25
  private async getTable() {
21
26
  const names = await this.db.tableNames();
@@ -70,63 +75,14 @@ export class MemoryRepository {
70
75
  }
71
76
  }
72
77
 
73
- /**
74
- * Ensures the FTS index exists on the content column.
75
- * Uses a mutex pattern to prevent concurrent index creation.
76
- * The key insight: we must capture the promise BEFORE any await.
77
- */
78
- private ensureFtsIndex(): Promise<void> {
79
- // If there's already a pending or completed index creation, return that promise
80
- if (this.ftsIndexPromise) {
81
- return this.ftsIndexPromise;
82
- }
83
-
84
- // Synchronously set the promise BEFORE any await
85
- // This is critical for proper mutex behavior in JS async code
86
- this.ftsIndexPromise = this.createFtsIndexIfNeeded().catch((error) => {
87
- // Reset on error so the next call can retry
88
- this.ftsIndexPromise = null;
89
- throw error;
90
- });
91
-
92
- return this.ftsIndexPromise;
93
- }
94
-
95
- /**
96
- * Creates the FTS index if it doesn't already exist.
97
- * Gets its own table reference to ensure consistent index state.
98
- */
99
- private async createFtsIndexIfNeeded(): Promise<void> {
100
- const table = await this.getTable();
101
- const indices = await table.listIndices();
102
- const hasFtsIndex = indices.some(
103
- (idx) => idx.columns.includes("content") && idx.indexType === "FTS"
104
- );
105
-
106
- if (!hasFtsIndex) {
107
- await table.createIndex("content", {
108
- config: Index.fts(),
109
- });
110
- // Wait for the index to be fully created and available
111
- await table.waitForIndex(["content_idx"], 30);
112
- }
113
- }
114
-
115
78
  /**
116
79
  * Converts a raw LanceDB row to a Memory object.
117
80
  */
118
81
  private rowToMemory(row: Record<string, unknown>): Memory {
119
- // Handle Arrow Vector type conversion
120
- // LanceDB returns an Arrow Vector object which is iterable but not an array
121
- const vectorData = row.vector as unknown;
122
- const embedding = Array.isArray(vectorData)
123
- ? vectorData
124
- : Array.from(vectorData as Iterable<number>) as number[];
125
-
126
82
  return {
127
83
  id: row.id as string,
128
84
  content: row.content as string,
129
- embedding,
85
+ embedding: arrowVectorToArray(row.vector),
130
86
  metadata: JSON.parse(row.metadata as string),
131
87
  createdAt: new Date(row.created_at as number),
132
88
  updatedAt: new Date(row.updated_at as number),
@@ -159,14 +115,14 @@ export class MemoryRepository {
159
115
 
160
116
  async upsert(memory: Memory): Promise<void> {
161
117
  const table = await this.getTable();
162
- const existing = await table.query().where(`id = '${memory.id}'`).limit(1).toArray();
118
+ const existing = await table.query().where(`id = '${escapeLanceDbString(memory.id)}'`).limit(1).toArray();
163
119
 
164
120
  if (existing.length === 0) {
165
121
  return await this.insert(memory);
166
122
  }
167
123
 
168
124
  await table.update({
169
- where: `id = '${memory.id}'`,
125
+ where: `id = '${escapeLanceDbString(memory.id)}'`,
170
126
  values: {
171
127
  vector: memory.embedding,
172
128
  content: memory.content,
@@ -183,7 +139,7 @@ export class MemoryRepository {
183
139
 
184
140
  async findById(id: string): Promise<Memory | null> {
185
141
  const table = await this.getTable();
186
- const results = await table.query().where(`id = '${id}'`).limit(1).toArray();
142
+ const results = await table.query().where(`id = '${escapeLanceDbString(id)}'`).limit(1).toArray();
187
143
 
188
144
  if (results.length === 0) {
189
145
  return null;
@@ -196,14 +152,14 @@ export class MemoryRepository {
196
152
  const table = await this.getTable();
197
153
 
198
154
  // Verify existence first to match previous behavior (return false if not found)
199
- const existing = await table.query().where(`id = '${id}'`).limit(1).toArray();
155
+ const existing = await table.query().where(`id = '${escapeLanceDbString(id)}'`).limit(1).toArray();
200
156
  if (existing.length === 0) {
201
157
  return false;
202
158
  }
203
159
 
204
160
  const now = Date.now();
205
161
  await table.update({
206
- where: `id = '${id}'`,
162
+ where: `id = '${escapeLanceDbString(id)}'`,
207
163
  values: {
208
164
  superseded_by: DELETED_TOMBSTONE,
209
165
  updated_at: now,
@@ -223,16 +179,11 @@ export class MemoryRepository {
223
179
  * @returns Array of HybridRow containing full Memory data plus RRF score
224
180
  */
225
181
  async findHybrid(embedding: number[], query: string, limit: number): Promise<HybridRow[]> {
226
- // Ensure FTS index exists (with mutex to prevent concurrent creation)
227
- // This must happen BEFORE getTable to ensure proper mutex behavior
228
182
  await this.ensureFtsIndex();
229
183
 
230
184
  const table = await this.getTable();
185
+ const reranker = await this.getReranker();
231
186
 
232
- // Create RRF reranker with k=60 (standard parameter)
233
- const reranker = await rerankers.RRFReranker.create(60);
234
-
235
- // Perform hybrid search: combine vector search and full-text search
236
187
  const results = await table
237
188
  .query()
238
189
  .nearestTo(embedding)
package/src/db/schema.ts CHANGED
@@ -9,34 +9,30 @@ import {
9
9
  Int32,
10
10
  } from "apache-arrow";
11
11
 
12
- export const TABLE_NAME = "memories";
12
+ // Shared field helpers — used by both memory and conversation history schemas
13
+ export const EMBEDDING_DIMENSION = 384;
13
14
 
14
- export const memorySchema = new Schema([
15
- new Field("id", new Utf8(), false),
15
+ export const vectorField = () =>
16
16
  new Field(
17
17
  "vector",
18
- new FixedSizeList(384, new Field("item", new Float32())),
18
+ new FixedSizeList(EMBEDDING_DIMENSION, new Field("item", new Float32())),
19
19
  false
20
- ),
20
+ );
21
+
22
+ export const timestampField = (name: string, nullable = false) =>
23
+ new Field(name, new Timestamp(TimeUnit.MILLISECOND), nullable);
24
+
25
+ export const TABLE_NAME = "memories";
26
+
27
+ export const memorySchema = new Schema([
28
+ new Field("id", new Utf8(), false),
29
+ vectorField(),
21
30
  new Field("content", new Utf8(), false),
22
31
  new Field("metadata", new Utf8(), false), // JSON string
23
- new Field(
24
- "created_at",
25
- new Timestamp(TimeUnit.MILLISECOND),
26
- false
27
- ),
28
- new Field(
29
- "updated_at",
30
- new Timestamp(TimeUnit.MILLISECOND),
31
- false
32
- ),
32
+ timestampField("created_at"),
33
+ timestampField("updated_at"),
33
34
  new Field("superseded_by", new Utf8(), true), // Nullable
34
35
  new Field("usefulness", new Float32(), false),
35
36
  new Field("access_count", new Int32(), false),
36
- new Field(
37
- "last_accessed",
38
- new Timestamp(TimeUnit.MILLISECOND),
39
- true
40
- ),
37
+ timestampField("last_accessed", true),
41
38
  ]);
42
-
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ import { connectToDatabase } from "./db/connection.js";
5
5
  import { MemoryRepository } from "./db/memory.repository.js";
6
6
  import { EmbeddingsService } from "./services/embeddings.service.js";
7
7
  import { MemoryService } from "./services/memory.service.js";
8
+ import { ConversationHistoryRepository } from "./db/conversation-history.repository.js";
9
+ import { ConversationHistoryService } from "./services/conversation-history.service.js";
8
10
  import { startServer } from "./mcp/server.js";
9
11
  import { startHttpServer } from "./http/server.js";
10
12
 
@@ -30,6 +32,20 @@ async function main(): Promise<void> {
30
32
  const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
31
33
  const memoryService = new MemoryService(repository, embeddings);
32
34
 
35
+ // Wire conversation history if enabled
36
+ if (config.conversationHistory.enabled) {
37
+ const historyRepo = new ConversationHistoryRepository(db);
38
+ const historyService = new ConversationHistoryService(
39
+ historyRepo,
40
+ embeddings,
41
+ config.conversationHistory.sessionPath,
42
+ );
43
+ memoryService.setConversationHistory(
44
+ historyService,
45
+ config.conversationHistory.historyWeight,
46
+ );
47
+ }
48
+
33
49
  // Track cleanup functions
34
50
  let httpStop: (() => void) | null = null;
35
51
 
@@ -1,6 +1,27 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import type { MemoryService } from "../services/memory.service.js";
3
+ import type { ConversationHistoryService } from "../services/conversation-history.service.js";
3
4
  import type { SearchIntent } from "../types/memory.js";
5
+ import type { SearchResult } from "../types/conversation-history.js";
6
+
7
+ /**
8
+ * Guard: returns the ConversationHistoryService or an error CallToolResult.
9
+ */
10
+ function requireHistoryService(
11
+ service: MemoryService,
12
+ ): ConversationHistoryService | CallToolResult {
13
+ const historyService = service.getConversationHistory();
14
+ if (!historyService) {
15
+ return {
16
+ content: [{
17
+ type: "text",
18
+ text: "Conversation history indexing is not enabled. Set conversationHistory.enabled = true in config.",
19
+ }],
20
+ isError: true,
21
+ };
22
+ }
23
+ return historyService;
24
+ }
4
25
 
5
26
  export async function handleStoreMemories(
6
27
  args: Record<string, unknown> | undefined,
@@ -103,9 +124,50 @@ export async function handleSearchMemories(
103
124
  ): Promise<CallToolResult> {
104
125
  const query = args?.query as string;
105
126
  const intent = (args?.intent as SearchIntent) ?? "fact_check";
106
- const _reasonForSearch = args?.reason_for_search as string; // Logged but not used in logic
107
127
  const limit = (args?.limit as number) ?? 10;
108
128
  const includeDeleted = (args?.include_deleted as boolean) ?? false;
129
+ const includeHistory = (args?.include_history as boolean) ?? false;
130
+ const historyOnly = (args?.history_only as boolean) ?? false;
131
+
132
+ if (includeHistory && historyOnly) {
133
+ return {
134
+ content: [{
135
+ type: "text",
136
+ text: "Cannot set both include_history and history_only to true. Use history_only for conversation history only, or include_history to merge with memories.",
137
+ }],
138
+ isError: true,
139
+ };
140
+ }
141
+
142
+ // History-only: search only conversation history via the history service
143
+ if (historyOnly) {
144
+ const result = requireHistoryService(service);
145
+ if ("content" in result) return result;
146
+ const historyResults = await result.search(query, limit);
147
+ if (historyResults.length === 0) {
148
+ return {
149
+ content: [{ type: "text", text: "No conversation history found matching your query." }],
150
+ };
151
+ }
152
+ return {
153
+ content: [{ type: "text", text: historyResults.map((r) => formatSearchResult(r)).join("\n\n---\n\n") }],
154
+ };
155
+ }
156
+
157
+ // Unified search: merge memories + history
158
+ if (includeHistory) {
159
+ const results = await service.searchUnified(query, intent, limit, includeDeleted);
160
+ if (results.length === 0) {
161
+ return {
162
+ content: [{ type: "text", text: "No results found matching your query." }],
163
+ };
164
+ }
165
+ return {
166
+ content: [{ type: "text", text: results.map((r) => formatSearchResult(r, includeDeleted)).join("\n\n---\n\n") }],
167
+ };
168
+ }
169
+
170
+ // Default: memory-only search (original behavior)
109
171
  const memories = await service.search(query, intent, limit, includeDeleted);
110
172
 
111
173
  if (memories.length === 0) {
@@ -130,36 +192,61 @@ export async function handleSearchMemories(
130
192
  };
131
193
  }
132
194
 
195
+ function formatMemoryDetail(
196
+ memoryId: string,
197
+ memory: Awaited<ReturnType<MemoryService["get"]>>
198
+ ): string {
199
+ if (!memory) {
200
+ return `Memory ${memoryId} not found`;
201
+ }
202
+
203
+ let result = `ID: ${memory.id}\nContent: ${memory.content}`;
204
+ if (Object.keys(memory.metadata).length > 0) {
205
+ result += `\nMetadata: ${JSON.stringify(memory.metadata)}`;
206
+ }
207
+ result += `\nCreated: ${memory.createdAt.toISOString()}`;
208
+ result += `\nUpdated: ${memory.updatedAt.toISOString()}`;
209
+ if (memory.supersededBy) {
210
+ result += `\nSuperseded by: ${memory.supersededBy}`;
211
+ }
212
+ return result;
213
+ }
214
+
215
+ /**
216
+ * Format a unified SearchResult (memory or history) for display.
217
+ * TODO: The default memory-only search path in handleSearchMemories formats results inline
218
+ * with similar logic but without the "Source:" label. Consolidating would add "Source: memory"
219
+ * to existing output, which may break consumers that parse it. (#5)
220
+ */
221
+ function formatSearchResult(result: SearchResult, includeDeleted: boolean = false): string {
222
+ if (result.source === "memory") {
223
+ let text = `ID: ${result.id}\nSource: memory\nContent: ${result.content}`;
224
+ if (Object.keys(result.metadata).length > 0) {
225
+ text += `\nMetadata: ${JSON.stringify(result.metadata)}`;
226
+ }
227
+ if (includeDeleted && result.supersededBy) {
228
+ text += `\n[DELETED]`;
229
+ }
230
+ return text;
231
+ }
232
+ // conversation_history
233
+ let text = `ID: ${result.id}\nSource: conversation_history\nSession: ${result.sessionId}\nRole: ${result.role}\nTimestamp: ${result.timestamp.toISOString()}\nContent: ${result.content}`;
234
+ if (Object.keys(result.metadata).length > 0) {
235
+ text += `\nMetadata: ${JSON.stringify(result.metadata)}`;
236
+ }
237
+ return text;
238
+ }
239
+
133
240
  export async function handleGetMemories(
134
241
  args: Record<string, unknown> | undefined,
135
242
  service: MemoryService
136
243
  ): Promise<CallToolResult> {
137
244
  const ids = args?.ids as string[];
138
245
 
139
- const format = (
140
- memoryId: string,
141
- memory: Awaited<ReturnType<MemoryService["get"]>>
142
- ) => {
143
- if (!memory) {
144
- return `Memory ${memoryId} not found`;
145
- }
146
-
147
- let result = `ID: ${memory.id}\nContent: ${memory.content}`;
148
- if (Object.keys(memory.metadata).length > 0) {
149
- result += `\nMetadata: ${JSON.stringify(memory.metadata)}`;
150
- }
151
- result += `\nCreated: ${memory.createdAt.toISOString()}`;
152
- result += `\nUpdated: ${memory.updatedAt.toISOString()}`;
153
- if (memory.supersededBy) {
154
- result += `\nSuperseded by: ${memory.supersededBy}`;
155
- }
156
- return result;
157
- };
158
-
159
246
  const blocks: string[] = [];
160
247
  for (const id of ids) {
161
248
  const memory = await service.get(id);
162
- blocks.push(format(id, memory));
249
+ blocks.push(formatMemoryDetail(id, memory));
163
250
  }
164
251
 
165
252
  return {
@@ -248,6 +335,69 @@ export async function handleGetCheckpoint(
248
335
  };
249
336
  }
250
337
 
338
+ export async function handleIndexConversations(
339
+ args: Record<string, unknown> | undefined,
340
+ service: MemoryService
341
+ ): Promise<CallToolResult> {
342
+ const result = requireHistoryService(service);
343
+ if ("content" in result) return result;
344
+
345
+ const path = args?.path as string | undefined;
346
+ const summary = await result.indexConversations(path);
347
+
348
+ return {
349
+ content: [{
350
+ type: "text",
351
+ text: `Indexing complete.\n- Sessions discovered: ${summary.sessionsDiscovered}\n- Sessions indexed: ${summary.sessionsIndexed}\n- Sessions skipped (unchanged): ${summary.sessionsSkipped}\n- Messages indexed: ${summary.messagesIndexed}`,
352
+ }],
353
+ };
354
+ }
355
+
356
+ export async function handleListIndexedSessions(
357
+ _args: Record<string, unknown> | undefined,
358
+ service: MemoryService
359
+ ): Promise<CallToolResult> {
360
+ const result = requireHistoryService(service);
361
+ if ("content" in result) return result;
362
+
363
+ const sessions = await result.listIndexedSessions();
364
+
365
+ if (sessions.length === 0) {
366
+ return {
367
+ content: [{ type: "text", text: "No indexed sessions found. Run index_conversations first." }],
368
+ };
369
+ }
370
+
371
+ const lines = sessions.map((s) => {
372
+ let line = `Session: ${s.sessionId}\n Messages: ${s.messageCount}\n First: ${s.firstMessageAt.toISOString()}\n Last: ${s.lastMessageAt.toISOString()}\n Indexed: ${s.indexedAt.toISOString()}`;
373
+ if (s.project) line += `\n Project: ${s.project}`;
374
+ if (s.gitBranch) line += `\n Branch: ${s.gitBranch}`;
375
+ return line;
376
+ });
377
+
378
+ return {
379
+ content: [{ type: "text", text: `${sessions.length} indexed session(s):\n\n${lines.join("\n\n")}` }],
380
+ };
381
+ }
382
+
383
+ export async function handleReindexSession(
384
+ args: Record<string, unknown> | undefined,
385
+ service: MemoryService
386
+ ): Promise<CallToolResult> {
387
+ const result = requireHistoryService(service);
388
+ if ("content" in result) return result;
389
+
390
+ const sessionId = args?.session_id as string;
391
+ const summary = await result.reindexSession(sessionId);
392
+
393
+ return {
394
+ content: [{
395
+ type: "text",
396
+ text: `Reindex complete for session ${sessionId}.\n- Messages indexed: ${summary.messagesIndexed}`,
397
+ }],
398
+ };
399
+ }
400
+
251
401
  export async function handleToolCall(
252
402
  name: string,
253
403
  args: Record<string, unknown> | undefined,
@@ -270,6 +420,12 @@ export async function handleToolCall(
270
420
  return handleStoreCheckpoint(args, service);
271
421
  case "get_checkpoint":
272
422
  return handleGetCheckpoint(args, service);
423
+ case "index_conversations":
424
+ return handleIndexConversations(args, service);
425
+ case "list_indexed_sessions":
426
+ return handleListIndexedSessions(args, service);
427
+ case "reindex_session":
428
+ return handleReindexSession(args, service);
273
429
  default:
274
430
  return {
275
431
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
package/src/mcp/tools.ts CHANGED
@@ -54,7 +54,7 @@ For long content (>1000 chars), provide embedding_text with a searchable summary
54
54
  },
55
55
  required: ["memories"],
56
56
  },
57
- };;
57
+ };
58
58
 
59
59
  export const deleteMemoriesTool: Tool = {
60
60
  name: "delete_memories",
@@ -74,7 +74,7 @@ export const deleteMemoriesTool: Tool = {
74
74
  },
75
75
  required: ["ids"],
76
76
  },
77
- };;
77
+ };
78
78
 
79
79
 
80
80
  const updateMemoriesTool: Tool = {
@@ -167,6 +167,20 @@ When in doubt, search. Missing context is costlier than an extra query.`,
167
167
  description: "Include soft-deleted memories in results (default: false). Useful for recovering prior information.",
168
168
  default: false,
169
169
  },
170
+ include_history: {
171
+ type: "boolean",
172
+ description:
173
+ "Include conversation history results alongside memories (default: false). " +
174
+ "Requires conversation history indexing to be enabled. History results are weighted lower than explicit memories.",
175
+ default: false,
176
+ },
177
+ history_only: {
178
+ type: "boolean",
179
+ description:
180
+ "Search only conversation history, excluding explicit memories (default: false). " +
181
+ "Requires conversation history indexing to be enabled.",
182
+ default: false,
183
+ },
170
184
  },
171
185
  required: ["query", "intent", "reason_for_search"],
172
186
  },
@@ -187,7 +201,7 @@ export const getMemoriesTool: Tool = {
187
201
  },
188
202
  required: ["ids"],
189
203
  },
190
- };;
204
+ };
191
205
 
192
206
  export const reportMemoryUsefulnessTool: Tool = {
193
207
  name: "report_memory_usefulness",
@@ -272,6 +286,52 @@ export const getCheckpointTool: Tool = {
272
286
  },
273
287
  };
274
288
 
289
+ export const indexConversationsTool: Tool = {
290
+ name: "index_conversations",
291
+ description:
292
+ "Index Claude Code session logs for searchable conversation history. " +
293
+ "Scans for JSONL session files, detects new/changed sessions, and indexes text messages. " +
294
+ "Skips unchanged files. Run periodically or after significant work sessions.",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ path: {
299
+ type: "string",
300
+ description:
301
+ "Directory containing session JSONL files. If omitted, auto-detects the Claude Code sessions directory.",
302
+ },
303
+ },
304
+ },
305
+ };
306
+
307
+ export const listIndexedSessionsTool: Tool = {
308
+ name: "list_indexed_sessions",
309
+ description:
310
+ "List all conversation sessions that have been indexed. " +
311
+ "Shows session ID, message count, time range, and associated project/branch.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {},
315
+ },
316
+ };
317
+
318
+ export const reindexSessionTool: Tool = {
319
+ name: "reindex_session",
320
+ description:
321
+ "Force a full re-index of a specific session. Deletes all existing entries for the session and re-parses from scratch. " +
322
+ "Use when session data appears corrupted or after parser improvements.",
323
+ inputSchema: {
324
+ type: "object",
325
+ properties: {
326
+ session_id: {
327
+ type: "string",
328
+ description: "The session ID to reindex.",
329
+ },
330
+ },
331
+ required: ["session_id"],
332
+ },
333
+ };
334
+
275
335
  export const tools: Tool[] = [
276
336
  storeMemoriesTool,
277
337
  updateMemoriesTool,
@@ -281,4 +341,7 @@ export const tools: Tool[] = [
281
341
  reportMemoryUsefulnessTool,
282
342
  storeCheckpointTool,
283
343
  getCheckpointTool,
344
+ indexConversationsTool,
345
+ listIndexedSessionsTool,
346
+ reindexSessionTool,
284
347
  ];