@prmichaelsen/remember-mcp 0.1.0 → 0.2.1

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.
@@ -11,35 +11,84 @@ import {
11
11
  McpError,
12
12
  } from '@modelcontextprotocol/sdk/types.js';
13
13
  import { logger } from './utils/logger.js';
14
+ import { initWeaviateClient } from './weaviate/client.js';
15
+ import { initFirestore } from './firestore/init.js';
14
16
 
15
17
  // Import memory tools
16
18
  import { createMemoryTool, handleCreateMemory } from './tools/create-memory.js';
17
19
  import { searchMemoryTool, handleSearchMemory } from './tools/search-memory.js';
18
20
  import { deleteMemoryTool, handleDeleteMemory } from './tools/delete-memory.js';
21
+ import { updateMemoryTool, handleUpdateMemory } from './tools/update-memory.js';
22
+ import { findSimilarTool, handleFindSimilar } from './tools/find-similar.js';
23
+ import { queryMemoryTool, handleQueryMemory } from './tools/query-memory.js';
24
+
25
+ // Import relationship tools
26
+ import { createRelationshipTool, handleCreateRelationship } from './tools/create-relationship.js';
27
+ import { updateRelationshipTool, handleUpdateRelationship } from './tools/update-relationship.js';
28
+ import { searchRelationshipTool, handleSearchRelationship } from './tools/search-relationship.js';
29
+ import { deleteRelationshipTool, handleDeleteRelationship } from './tools/delete-relationship.js';
19
30
 
20
31
  export interface ServerOptions {
21
32
  name?: string;
22
33
  version?: string;
23
34
  }
24
35
 
36
+ // Global initialization flag to ensure databases are initialized once
37
+ let databasesInitialized = false;
38
+ let initializationPromise: Promise<void> | null = null;
39
+
40
+ /**
41
+ * Initialize databases (called once globally)
42
+ */
43
+ async function ensureDatabasesInitialized(): Promise<void> {
44
+ if (databasesInitialized) {
45
+ return;
46
+ }
47
+
48
+ // If initialization is in progress, wait for it
49
+ if (initializationPromise) {
50
+ return initializationPromise;
51
+ }
52
+
53
+ // Start initialization
54
+ initializationPromise = (async () => {
55
+ try {
56
+ logger.info('Initializing databases...');
57
+ await initWeaviateClient();
58
+ initFirestore();
59
+ databasesInitialized = true;
60
+ logger.info('Databases initialized successfully');
61
+ } catch (error) {
62
+ logger.error('Database initialization failed:', error);
63
+ throw error;
64
+ } finally {
65
+ initializationPromise = null;
66
+ }
67
+ })();
68
+
69
+ return initializationPromise;
70
+ }
71
+
25
72
  /**
26
73
  * Create a server instance for a specific user/tenant
27
- *
74
+ *
28
75
  * This factory function is compatible with mcp-auth wrapping pattern.
29
76
  * It creates isolated server instances with no shared state.
30
- *
77
+ *
78
+ * Note: Databases (Weaviate + Firestore) are initialized once globally on first call.
79
+ *
31
80
  * @param accessToken - User's access token (reserved for future external APIs)
32
81
  * @param userId - User identifier for scoping operations
33
82
  * @param options - Optional server configuration
34
83
  * @returns Configured MCP Server instance (not connected to transport)
35
- *
84
+ *
36
85
  * @example
37
86
  * ```typescript
38
87
  * // Direct usage
39
88
  * const server = createServer('token', 'user123');
40
89
  * const transport = new StdioServerTransport();
41
90
  * await server.connect(transport);
42
- *
91
+ *
43
92
  * // With mcp-auth
44
93
  * import { wrapServer } from '@prmichaelsen/mcp-auth';
45
94
  * const wrapped = wrapServer({
@@ -66,11 +115,18 @@ export function createServer(
66
115
 
67
116
  logger.debug('Creating server instance', { userId });
68
117
 
118
+ // Ensure databases are initialized (happens once globally)
119
+ // Note: This is synchronous to match the Server return type
120
+ // The actual initialization happens on first tool call
121
+ ensureDatabasesInitialized().catch(error => {
122
+ logger.error('Failed to initialize databases:', error);
123
+ });
124
+
69
125
  // Create MCP server
70
126
  const server = new Server(
71
127
  {
72
128
  name: options.name || 'remember-mcp',
73
- version: options.version || '0.1.0',
129
+ version: options.version || '0.2.0',
74
130
  },
75
131
  {
76
132
  capabilities: {
@@ -101,9 +157,18 @@ function registerHandlers(server: Server, userId: string, accessToken: string):
101
157
  properties: {},
102
158
  },
103
159
  },
160
+ // Memory tools
104
161
  createMemoryTool,
105
162
  searchMemoryTool,
106
163
  deleteMemoryTool,
164
+ updateMemoryTool,
165
+ findSimilarTool,
166
+ queryMemoryTool,
167
+ // Relationship tools
168
+ createRelationshipTool,
169
+ updateRelationshipTool,
170
+ searchRelationshipTool,
171
+ deleteRelationshipTool,
107
172
  ],
108
173
  };
109
174
  });
@@ -132,6 +197,34 @@ function registerHandlers(server: Server, userId: string, accessToken: string):
132
197
  result = await handleDeleteMemory(args as any, userId);
133
198
  break;
134
199
 
200
+ case 'remember_update_memory':
201
+ result = await handleUpdateMemory(args as any, userId);
202
+ break;
203
+
204
+ case 'remember_find_similar':
205
+ result = await handleFindSimilar(args as any, userId);
206
+ break;
207
+
208
+ case 'remember_query_memory':
209
+ result = await handleQueryMemory(args as any, userId);
210
+ break;
211
+
212
+ case 'remember_create_relationship':
213
+ result = await handleCreateRelationship(args as any, userId);
214
+ break;
215
+
216
+ case 'remember_update_relationship':
217
+ result = await handleUpdateRelationship(args as any, userId);
218
+ break;
219
+
220
+ case 'remember_search_relationship':
221
+ result = await handleSearchRelationship(args as any, userId);
222
+ break;
223
+
224
+ case 'remember_delete_relationship':
225
+ result = await handleDeleteRelationship(args as any, userId);
226
+ break;
227
+
135
228
  default:
136
229
  throw new McpError(
137
230
  ErrorCode.MethodNotFound,
package/src/server.ts CHANGED
@@ -17,6 +17,15 @@ import { logger } from './utils/logger.js';
17
17
  import { createMemoryTool, handleCreateMemory } from './tools/create-memory.js';
18
18
  import { searchMemoryTool, handleSearchMemory } from './tools/search-memory.js';
19
19
  import { deleteMemoryTool, handleDeleteMemory } from './tools/delete-memory.js';
20
+ import { updateMemoryTool, handleUpdateMemory } from './tools/update-memory.js';
21
+ import { findSimilarTool, handleFindSimilar } from './tools/find-similar.js';
22
+ import { queryMemoryTool, handleQueryMemory } from './tools/query-memory.js';
23
+
24
+ // Import relationship tools
25
+ import { createRelationshipTool, handleCreateRelationship } from './tools/create-relationship.js';
26
+ import { updateRelationshipTool, handleUpdateRelationship } from './tools/update-relationship.js';
27
+ import { searchRelationshipTool, handleSearchRelationship } from './tools/search-relationship.js';
28
+ import { deleteRelationshipTool, handleDeleteRelationship } from './tools/delete-relationship.js';
20
29
 
21
30
  /**
22
31
  * Initialize remember-mcp server
@@ -78,9 +87,18 @@ function registerHandlers(server: Server): void {
78
87
  properties: {},
79
88
  },
80
89
  },
90
+ // Memory tools
81
91
  createMemoryTool,
82
92
  searchMemoryTool,
83
93
  deleteMemoryTool,
94
+ updateMemoryTool,
95
+ findSimilarTool,
96
+ queryMemoryTool,
97
+ // Relationship tools
98
+ createRelationshipTool,
99
+ updateRelationshipTool,
100
+ searchRelationshipTool,
101
+ deleteRelationshipTool,
84
102
  ],
85
103
  };
86
104
  });
@@ -113,6 +131,34 @@ function registerHandlers(server: Server): void {
113
131
  result = await handleDeleteMemory(args as any, userId);
114
132
  break;
115
133
 
134
+ case 'remember_update_memory':
135
+ result = await handleUpdateMemory(args as any, userId);
136
+ break;
137
+
138
+ case 'remember_find_similar':
139
+ result = await handleFindSimilar(args as any, userId);
140
+ break;
141
+
142
+ case 'remember_query_memory':
143
+ result = await handleQueryMemory(args as any, userId);
144
+ break;
145
+
146
+ case 'remember_create_relationship':
147
+ result = await handleCreateRelationship(args as any, userId);
148
+ break;
149
+
150
+ case 'remember_update_relationship':
151
+ result = await handleUpdateRelationship(args as any, userId);
152
+ break;
153
+
154
+ case 'remember_search_relationship':
155
+ result = await handleSearchRelationship(args as any, userId);
156
+ break;
157
+
158
+ case 'remember_delete_relationship':
159
+ result = await handleDeleteRelationship(args as any, userId);
160
+ break;
161
+
116
162
  default:
117
163
  throw new McpError(
118
164
  ErrorCode.MethodNotFound,
@@ -0,0 +1,242 @@
1
+ /**
2
+ * remember_create_relationship tool
3
+ * Create a relationship connecting 2...N memories
4
+ */
5
+
6
+ import type { Relationship, MemoryContext } from '../types/memory.js';
7
+ import { ensureMemoryCollection, getMemoryCollection } from '../weaviate/schema.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * Tool definition for remember_create_relationship
12
+ */
13
+ export const createRelationshipTool = {
14
+ name: 'remember_create_relationship',
15
+ description: `Create a relationship connecting 2 or more memories.
16
+
17
+ Relationships describe how memories are connected with free-form types.
18
+ Each relationship has an observation (description), strength, and confidence.
19
+ Bidirectional: connected memories are automatically updated with the relationship ID.
20
+
21
+ Examples:
22
+ - "The Yosemite trip inspired my Sequoia planning"
23
+ - "My tent purchase was caused by the camping trip"
24
+ - "This recipe contradicts my previous cooking notes"
25
+ `,
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ memory_ids: {
30
+ type: 'array',
31
+ items: { type: 'string' },
32
+ description: 'Array of 2 or more memory IDs to connect',
33
+ minItems: 2,
34
+ },
35
+ relationship_type: {
36
+ type: 'string',
37
+ description: 'Type of relationship (free-form): "inspired_by", "contradicts", "caused_by", "related_to", etc.',
38
+ },
39
+ observation: {
40
+ type: 'string',
41
+ description: 'Description of the connection (will be vectorized for semantic search)',
42
+ },
43
+ strength: {
44
+ type: 'number',
45
+ description: 'Strength of the relationship (0-1, default: 0.5)',
46
+ minimum: 0,
47
+ maximum: 1,
48
+ },
49
+ confidence: {
50
+ type: 'number',
51
+ description: 'Confidence in this relationship (0-1, default: 0.8)',
52
+ minimum: 0,
53
+ maximum: 1,
54
+ },
55
+ tags: {
56
+ type: 'array',
57
+ items: { type: 'string' },
58
+ description: 'Tags for organization',
59
+ },
60
+ },
61
+ required: ['memory_ids', 'relationship_type', 'observation'],
62
+ },
63
+ };
64
+
65
+ /**
66
+ * Create relationship arguments
67
+ */
68
+ export interface CreateRelationshipArgs {
69
+ memory_ids: string[];
70
+ relationship_type: string;
71
+ observation: string;
72
+ strength?: number;
73
+ confidence?: number;
74
+ tags?: string[];
75
+ }
76
+
77
+ /**
78
+ * Create relationship result
79
+ */
80
+ export interface CreateRelationshipResult {
81
+ relationship_id: string;
82
+ memory_ids: string[];
83
+ relationship_type: string;
84
+ created_at: string;
85
+ message: string;
86
+ }
87
+
88
+ /**
89
+ * Handle remember_create_relationship tool
90
+ */
91
+ export async function handleCreateRelationship(
92
+ args: CreateRelationshipArgs,
93
+ userId: string,
94
+ context?: Partial<MemoryContext>
95
+ ): Promise<string> {
96
+ try {
97
+ logger.info('Creating relationship', {
98
+ userId,
99
+ type: args.relationship_type,
100
+ memoryCount: args.memory_ids.length
101
+ });
102
+
103
+ // Validate memory_ids count
104
+ if (args.memory_ids.length < 2) {
105
+ throw new Error('At least 2 memory IDs are required to create a relationship');
106
+ }
107
+
108
+ // Ensure collection exists
109
+ await ensureMemoryCollection(userId);
110
+ const collection = getMemoryCollection(userId);
111
+
112
+ // Verify all memories exist and belong to user
113
+ const memoryChecks = await Promise.all(
114
+ args.memory_ids.map(async (memoryId) => {
115
+ try {
116
+ const memory = await collection.query.fetchObjectById(memoryId, {
117
+ returnProperties: ['user_id', 'doc_type', 'relationships'],
118
+ });
119
+
120
+ if (!memory) {
121
+ return { memoryId, error: 'Memory not found' };
122
+ }
123
+
124
+ if (memory.properties.user_id !== userId) {
125
+ return { memoryId, error: 'Unauthorized: Memory belongs to another user' };
126
+ }
127
+
128
+ if (memory.properties.doc_type !== 'memory') {
129
+ return { memoryId, error: 'Cannot create relationship with non-memory document' };
130
+ }
131
+
132
+ return {
133
+ memoryId,
134
+ memory,
135
+ relationships: (memory.properties.relationships as string[]) || []
136
+ };
137
+ } catch (error) {
138
+ return { memoryId, error: `Failed to fetch memory: ${error}` };
139
+ }
140
+ })
141
+ );
142
+
143
+ // Check for errors
144
+ const errors = memoryChecks.filter(check => check.error);
145
+ if (errors.length > 0) {
146
+ const errorMessages = errors.map(e => `${e.memoryId}: ${e.error}`).join('; ');
147
+ throw new Error(`Memory validation failed: ${errorMessages}`);
148
+ }
149
+
150
+ // Build relationship object
151
+ const now = new Date().toISOString();
152
+ const relationship: Omit<Relationship, 'id'> = {
153
+ // Core identity
154
+ user_id: userId,
155
+ doc_type: 'relationship',
156
+
157
+ // Connection
158
+ memory_ids: args.memory_ids,
159
+ relationship_type: args.relationship_type,
160
+
161
+ // Observation
162
+ observation: args.observation,
163
+ strength: args.strength ?? 0.5,
164
+ confidence: args.confidence ?? 0.8,
165
+
166
+ // Context
167
+ context: {
168
+ timestamp: now,
169
+ source: {
170
+ type: 'api',
171
+ platform: 'mcp',
172
+ },
173
+ summary: context?.summary || 'Relationship created via MCP',
174
+ conversation_id: context?.conversation_id,
175
+ ...context,
176
+ },
177
+
178
+ // Metadata
179
+ created_at: now,
180
+ updated_at: now,
181
+ version: 1,
182
+ tags: args.tags || [],
183
+ };
184
+
185
+ // Insert relationship into Weaviate
186
+ const relationshipId = await collection.data.insert(relationship as any);
187
+
188
+ logger.info('Relationship created, updating connected memories', {
189
+ relationshipId,
190
+ userId
191
+ });
192
+
193
+ // Update all connected memories with bidirectional reference
194
+ const updatePromises = memoryChecks
195
+ .filter(check => !check.error && check.memory)
196
+ .map(async (check) => {
197
+ try {
198
+ const existingRelationships = check.relationships || [];
199
+ const updatedRelationships = [...existingRelationships, relationshipId];
200
+
201
+ await collection.data.update({
202
+ id: check.memoryId,
203
+ properties: {
204
+ relationships: updatedRelationships,
205
+ updated_at: now,
206
+ },
207
+ });
208
+
209
+ return { memoryId: check.memoryId, success: true };
210
+ } catch (error) {
211
+ logger.warn(`Failed to update memory ${check.memoryId} with relationship:`, error);
212
+ return { memoryId: check.memoryId, success: false, error };
213
+ }
214
+ });
215
+
216
+ const updateResults = await Promise.all(updatePromises);
217
+ const failedUpdates = updateResults.filter(r => !r.success);
218
+
219
+ if (failedUpdates.length > 0) {
220
+ logger.warn('Some memory updates failed', { failedUpdates });
221
+ }
222
+
223
+ logger.info('Relationship created successfully', {
224
+ relationshipId,
225
+ userId,
226
+ updatedMemories: updateResults.filter(r => r.success).length
227
+ });
228
+
229
+ const response: CreateRelationshipResult = {
230
+ relationship_id: relationshipId,
231
+ memory_ids: args.memory_ids,
232
+ relationship_type: args.relationship_type,
233
+ created_at: now,
234
+ message: `Relationship created successfully with ID: ${relationshipId}. Connected ${args.memory_ids.length} memories.`,
235
+ };
236
+
237
+ return JSON.stringify(response, null, 2);
238
+ } catch (error) {
239
+ logger.error('Failed to create relationship:', error);
240
+ throw new Error(`Failed to create relationship: ${error instanceof Error ? error.message : String(error)}`);
241
+ }
242
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * remember_delete_relationship tool
3
+ * Delete a relationship and clean up references in connected memories
4
+ */
5
+
6
+ import { getMemoryCollection } from '../weaviate/schema.js';
7
+ import { logger } from '../utils/logger.js';
8
+
9
+ /**
10
+ * Tool definition for remember_delete_relationship
11
+ */
12
+ export const deleteRelationshipTool = {
13
+ name: 'remember_delete_relationship',
14
+ description: `Delete a relationship from your collection.
15
+
16
+ Automatically removes the relationship reference from all connected memories.
17
+ This action cannot be undone.
18
+
19
+ Examples:
20
+ - "Delete that inspiration relationship"
21
+ - "Remove the connection between those memories"
22
+ `,
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ relationship_id: {
27
+ type: 'string',
28
+ description: 'ID of the relationship to delete',
29
+ },
30
+ },
31
+ required: ['relationship_id'],
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Delete relationship arguments
37
+ */
38
+ export interface DeleteRelationshipArgs {
39
+ relationship_id: string;
40
+ }
41
+
42
+ /**
43
+ * Delete relationship result
44
+ */
45
+ export interface DeleteRelationshipResult {
46
+ relationship_id: string;
47
+ deleted: boolean;
48
+ memories_updated: number;
49
+ message: string;
50
+ }
51
+
52
+ /**
53
+ * Handle remember_delete_relationship tool
54
+ */
55
+ export async function handleDeleteRelationship(
56
+ args: DeleteRelationshipArgs,
57
+ userId: string
58
+ ): Promise<string> {
59
+ try {
60
+ logger.info('Deleting relationship', { userId, relationshipId: args.relationship_id });
61
+
62
+ const collection = getMemoryCollection(userId);
63
+
64
+ // Get relationship to verify ownership and get connected memory IDs
65
+ const relationship = await collection.query.fetchObjectById(args.relationship_id, {
66
+ returnProperties: ['user_id', 'doc_type', 'memory_ids', 'relationship_type'],
67
+ });
68
+
69
+ if (!relationship) {
70
+ throw new Error(`Relationship not found: ${args.relationship_id}`);
71
+ }
72
+
73
+ // Verify ownership
74
+ if (relationship.properties.user_id !== userId) {
75
+ throw new Error('Unauthorized: Cannot delete another user\'s relationship');
76
+ }
77
+
78
+ // Verify it's a relationship (not a memory)
79
+ if (relationship.properties.doc_type !== 'relationship') {
80
+ throw new Error('Cannot delete memories using this tool. Use remember_delete_memory instead.');
81
+ }
82
+
83
+ const memoryIds = (relationship.properties.memory_ids as string[]) || [];
84
+ let memoriesUpdated = 0;
85
+
86
+ // Remove relationship reference from all connected memories
87
+ if (memoryIds.length > 0) {
88
+ logger.info('Cleaning up relationship references from connected memories', {
89
+ relationshipId: args.relationship_id,
90
+ memoryCount: memoryIds.length,
91
+ });
92
+
93
+ const updatePromises = memoryIds.map(async (memoryId) => {
94
+ try {
95
+ // Get current memory to read relationships array
96
+ const memory = await collection.query.fetchObjectById(memoryId, {
97
+ returnProperties: ['relationships', 'doc_type'],
98
+ });
99
+
100
+ if (!memory || memory.properties.doc_type !== 'memory') {
101
+ logger.warn(`Memory ${memoryId} not found or not a memory, skipping cleanup`);
102
+ return { memoryId, success: false };
103
+ }
104
+
105
+ const currentRelationships = (memory.properties.relationships as string[]) || [];
106
+ const updatedRelationships = currentRelationships.filter(
107
+ (relId) => relId !== args.relationship_id
108
+ );
109
+
110
+ // Only update if there was a change
111
+ if (updatedRelationships.length !== currentRelationships.length) {
112
+ await collection.data.update({
113
+ id: memoryId,
114
+ properties: {
115
+ relationships: updatedRelationships,
116
+ updated_at: new Date().toISOString(),
117
+ },
118
+ });
119
+ return { memoryId, success: true };
120
+ }
121
+
122
+ return { memoryId, success: false };
123
+ } catch (error) {
124
+ logger.warn(`Failed to update memory ${memoryId}:`, error);
125
+ return { memoryId, success: false, error };
126
+ }
127
+ });
128
+
129
+ const updateResults = await Promise.all(updatePromises);
130
+ memoriesUpdated = updateResults.filter((r) => r.success).length;
131
+
132
+ logger.info('Memory cleanup completed', {
133
+ relationshipId: args.relationship_id,
134
+ memoriesUpdated,
135
+ totalMemories: memoryIds.length,
136
+ });
137
+ }
138
+
139
+ // Delete the relationship
140
+ await collection.data.deleteById(args.relationship_id);
141
+
142
+ logger.info('Relationship deleted successfully', {
143
+ userId,
144
+ relationshipId: args.relationship_id,
145
+ memoriesUpdated,
146
+ });
147
+
148
+ const result: DeleteRelationshipResult = {
149
+ relationship_id: args.relationship_id,
150
+ deleted: true,
151
+ memories_updated: memoriesUpdated,
152
+ message: `Relationship deleted successfully${memoriesUpdated > 0 ? ` (${memoriesUpdated} memories updated)` : ''}`,
153
+ };
154
+
155
+ return JSON.stringify(result, null, 2);
156
+ } catch (error) {
157
+ logger.error('Failed to delete relationship:', error);
158
+ throw new Error(`Failed to delete relationship: ${error instanceof Error ? error.message : String(error)}`);
159
+ }
160
+ }