purmemo-mcp 1.0.0 β†’ 2.0.0

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.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "purmemo-mcp",
3
- "version": "1.0.0",
4
- "description": "Official Model Context Protocol (MCP) server for Purmemo - Your AI-powered second brain with 94% memory retrieval accuracy",
5
- "main": "src/index.js",
3
+ "version": "2.0.0",
4
+ "description": "Official Model Context Protocol (MCP) server for Purmemo - Seamless OAuth authentication for your AI-powered second brain",
5
+ "main": "src/server-oauth.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "purmemo-mcp": "./bin/purmemo-mcp"
8
+ "purmemo-mcp": "./src/server-oauth.js",
9
+ "purmemo-mcp-setup": "./src/setup.js"
9
10
  },
10
11
  "files": [
11
12
  "src/",
@@ -14,35 +15,48 @@
14
15
  "LICENSE"
15
16
  ],
16
17
  "scripts": {
17
- "start": "node src/index.js"
18
+ "start": "node src/server-oauth.js",
19
+ "setup": "node src/setup.js setup",
20
+ "status": "node src/setup.js status",
21
+ "logout": "node src/setup.js logout",
22
+ "test": "echo \"No tests yet\"",
23
+ "postinstall": "node -e \"console.log('\\n🧠 pūrmemo MCP v2.0.0 installed!\\n\\nNew users: Run \\'npx purmemo-mcp setup\\' to connect your account.\\nExisting users: Your API key still works (backwards compatible).\\n')\""
18
24
  },
19
25
  "keywords": [
20
26
  "mcp",
21
27
  "memory",
22
28
  "ai",
23
29
  "claude",
24
- "purmemo"
30
+ "purmemo",
31
+ "oauth",
32
+ "authentication"
25
33
  ],
26
- "author": "Christopher Coladapo <support@purmemo.ai>",
34
+ "author": "Purmemo Team <support@purmemo.ai>",
27
35
  "license": "MIT",
28
36
  "repository": {
29
37
  "type": "git",
30
38
  "url": "git+https://github.com/coladapo/purmemo-mcp.git"
31
39
  },
32
40
  "bugs": {
33
- "url": "https://github.com/coladapo/puo-memo-mcp/issues",
41
+ "url": "https://github.com/coladapo/purmemo-mcp/issues",
34
42
  "email": "support@purmemo.ai"
35
43
  },
36
44
  "funding": {
37
45
  "type": "github",
38
46
  "url": "https://github.com/sponsors/coladapo"
39
47
  },
40
- "support": "https://github.com/coladapo/puo-memo-mcp/discussions",
48
+ "support": "https://github.com/coladapo/purmemo-mcp/discussions",
41
49
  "dependencies": {
42
50
  "@modelcontextprotocol/sdk": "^1.16.0",
43
- "node-fetch": "^3.3.2"
51
+ "node-fetch": "^3.3.2",
52
+ "express": "^4.18.2",
53
+ "open": "^10.0.0",
54
+ "commander": "^11.1.0",
55
+ "chalk": "^5.3.0",
56
+ "inquirer": "^9.2.12",
57
+ "ora": "^7.0.1"
44
58
  },
45
59
  "engines": {
46
60
  "node": ">=18.0.0"
47
61
  }
48
- }
62
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * OAuth Manager for Purmemo MCP
3
+ * Handles OAuth 2.1 + PKCE flow for seamless authentication
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import express from 'express';
8
+ import open from 'open';
9
+ import TokenStore from './token-store.js';
10
+
11
+ class OAuthManager {
12
+ constructor(config = {}) {
13
+ this.apiUrl = config.apiUrl || process.env.PUO_MEMO_API_URL || 'https://api.purmemo.ai';
14
+ this.clientId = config.clientId || 'purmemo-mcp';
15
+ this.redirectUri = config.redirectUri || 'http://localhost:3456/callback';
16
+ this.tokenStore = new TokenStore();
17
+ this.server = null;
18
+ this.pendingAuth = null;
19
+ }
20
+
21
+ /**
22
+ * Get current authentication token
23
+ * @returns {Promise<string|null>} Access token or null if not authenticated
24
+ */
25
+ async getToken() {
26
+ // First check if we have a valid token
27
+ const storedToken = await this.tokenStore.getToken();
28
+
29
+ if (storedToken && storedToken.access_token) {
30
+ // Check if token needs refresh (expired or close to expiry)
31
+ if (this.isTokenExpired(storedToken)) {
32
+ try {
33
+ return await this.refreshToken(storedToken.refresh_token);
34
+ } catch (error) {
35
+ console.error('Token refresh failed:', error.message);
36
+ // If refresh fails, start new OAuth flow
37
+ return null;
38
+ }
39
+ }
40
+ return storedToken.access_token;
41
+ }
42
+
43
+ // Fallback to environment variable for backwards compatibility
44
+ const envApiKey = process.env.PUO_MEMO_API_KEY;
45
+ if (envApiKey) {
46
+ console.log('πŸ“” Using API key from environment variable');
47
+ return envApiKey;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Check if token is expired or about to expire
55
+ */
56
+ isTokenExpired(token) {
57
+ if (!token.expires_at) return false;
58
+
59
+ const now = Date.now();
60
+ const expiresAt = new Date(token.expires_at).getTime();
61
+ const bufferTime = 5 * 60 * 1000; // 5 minutes buffer
62
+
63
+ return now >= (expiresAt - bufferTime);
64
+ }
65
+
66
+ /**
67
+ * Start OAuth flow
68
+ * @returns {Promise<string>} Access token
69
+ */
70
+ async authenticate() {
71
+ if (this.pendingAuth) {
72
+ return this.pendingAuth;
73
+ }
74
+
75
+ this.pendingAuth = this.performOAuthFlow();
76
+
77
+ try {
78
+ const token = await this.pendingAuth;
79
+ return token;
80
+ } finally {
81
+ this.pendingAuth = null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Perform the actual OAuth flow
87
+ */
88
+ async performOAuthFlow() {
89
+ console.log('\nπŸ” Starting Purmemo authentication...\n');
90
+
91
+ // Generate PKCE challenge
92
+ const codeVerifier = this.generateCodeVerifier();
93
+ const codeChallenge = this.generateCodeChallenge(codeVerifier);
94
+ const state = crypto.randomBytes(16).toString('hex');
95
+
96
+ // Build OAuth URL
97
+ const authUrl = new URL(`${this.apiUrl}/api/oauth/initiate`);
98
+ authUrl.searchParams.append('client_id', this.clientId);
99
+ authUrl.searchParams.append('redirect_uri', this.redirectUri);
100
+ authUrl.searchParams.append('response_type', 'code');
101
+ authUrl.searchParams.append('scope', 'memories.read memories.write entities.read');
102
+ authUrl.searchParams.append('state', state);
103
+ authUrl.searchParams.append('code_challenge', codeChallenge);
104
+ authUrl.searchParams.append('code_challenge_method', 'S256');
105
+
106
+ // Start local server for callback
107
+ const authCode = await this.startCallbackServer(state);
108
+
109
+ // Open browser for authentication
110
+ console.log('πŸ“± Opening browser for authentication...');
111
+ console.log(' If browser doesn\'t open, visit:');
112
+ console.log(` ${authUrl.toString()}\n`);
113
+
114
+ await open(authUrl.toString());
115
+
116
+ // Wait for callback with auth code
117
+ const code = await authCode;
118
+
119
+ // Exchange code for token
120
+ const tokenResponse = await this.exchangeCodeForToken(code, codeVerifier);
121
+
122
+ // Store token securely
123
+ await this.tokenStore.saveToken(tokenResponse);
124
+
125
+ console.log('βœ… Authentication successful!\n');
126
+
127
+ return tokenResponse.access_token;
128
+ }
129
+
130
+ /**
131
+ * Start local server to handle OAuth callback
132
+ */
133
+ startCallbackServer(expectedState) {
134
+ return new Promise((resolve, reject) => {
135
+ const app = express();
136
+ let resolved = false;
137
+
138
+ app.get('/callback', async (req, res) => {
139
+ const { code, state, error, error_description } = req.query;
140
+
141
+ if (error) {
142
+ res.send(`
143
+ <html>
144
+ <head>
145
+ <title>Authentication Failed</title>
146
+ <style>
147
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
148
+ display: flex; justify-content: center; align-items: center;
149
+ height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
150
+ .container { background: white; padding: 40px; border-radius: 10px;
151
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; }
152
+ h1 { color: #e53e3e; margin: 0 0 10px 0; }
153
+ p { color: #718096; margin: 10px 0; }
154
+ .error { background: #fed7d7; padding: 10px; border-radius: 5px;
155
+ color: #c53030; margin-top: 20px; }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <div class="container">
160
+ <h1>❌ Authentication Failed</h1>
161
+ <p>Unable to complete authentication</p>
162
+ <div class="error">${error}: ${error_description || 'Unknown error'}</div>
163
+ <p style="margin-top: 20px; font-size: 14px;">You can close this window</p>
164
+ </div>
165
+ </body>
166
+ </html>
167
+ `);
168
+
169
+ if (!resolved) {
170
+ resolved = true;
171
+ this.stopCallbackServer();
172
+ reject(new Error(`OAuth error: ${error} - ${error_description}`));
173
+ }
174
+ return;
175
+ }
176
+
177
+ if (state !== expectedState) {
178
+ res.send(`
179
+ <html>
180
+ <head><title>Security Error</title></head>
181
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
182
+ <h1 style="color: red;">Security Error</h1>
183
+ <p>State mismatch - possible CSRF attack</p>
184
+ </body>
185
+ </html>
186
+ `);
187
+
188
+ if (!resolved) {
189
+ resolved = true;
190
+ this.stopCallbackServer();
191
+ reject(new Error('OAuth state mismatch'));
192
+ }
193
+ return;
194
+ }
195
+
196
+ // Success response
197
+ res.send(`
198
+ <html>
199
+ <head>
200
+ <title>Authentication Successful</title>
201
+ <style>
202
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
203
+ display: flex; justify-content: center; align-items: center;
204
+ height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
205
+ .container { background: white; padding: 40px; border-radius: 10px;
206
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; }
207
+ h1 { color: #48bb78; margin: 0 0 10px 0; }
208
+ p { color: #718096; margin: 10px 0; }
209
+ .success { background: #c6f6d5; padding: 15px; border-radius: 5px;
210
+ color: #22543d; margin-top: 20px; }
211
+ .logo { font-size: 48px; margin-bottom: 20px; }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="container">
216
+ <div class="logo">🧠</div>
217
+ <h1>βœ… Authentication Successful!</h1>
218
+ <p>You're now connected to Purmemo</p>
219
+ <div class="success">
220
+ You can now close this window and return to Claude Desktop
221
+ </div>
222
+ <script>setTimeout(() => window.close(), 3000);</script>
223
+ </div>
224
+ </body>
225
+ </html>
226
+ `);
227
+
228
+ if (!resolved) {
229
+ resolved = true;
230
+ this.stopCallbackServer();
231
+ resolve(code);
232
+ }
233
+ });
234
+
235
+ // Start server
236
+ this.server = app.listen(3456, () => {
237
+ console.log('🌐 Waiting for authentication callback...\n');
238
+ });
239
+
240
+ // Timeout after 5 minutes
241
+ setTimeout(() => {
242
+ if (!resolved) {
243
+ resolved = true;
244
+ this.stopCallbackServer();
245
+ reject(new Error('Authentication timeout'));
246
+ }
247
+ }, 5 * 60 * 1000);
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Stop the callback server
253
+ */
254
+ stopCallbackServer() {
255
+ if (this.server) {
256
+ this.server.close();
257
+ this.server = null;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Exchange authorization code for access token
263
+ */
264
+ async exchangeCodeForToken(code, codeVerifier) {
265
+ const response = await fetch(`${this.apiUrl}/api/oauth/token`, {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Content-Type': 'application/json',
269
+ 'User-Agent': 'purmemo-mcp/2.0.0'
270
+ },
271
+ body: JSON.stringify({
272
+ grant_type: 'authorization_code',
273
+ code,
274
+ client_id: this.clientId,
275
+ redirect_uri: this.redirectUri,
276
+ code_verifier: codeVerifier
277
+ })
278
+ });
279
+
280
+ if (!response.ok) {
281
+ const error = await response.text();
282
+ throw new Error(`Token exchange failed: ${error}`);
283
+ }
284
+
285
+ const tokenData = await response.json();
286
+
287
+ // Add expiry time
288
+ if (tokenData.expires_in) {
289
+ tokenData.expires_at = new Date(Date.now() + tokenData.expires_in * 1000).toISOString();
290
+ }
291
+
292
+ // Store user tier info
293
+ if (tokenData.user) {
294
+ tokenData.user_tier = tokenData.user.tier || 'free';
295
+ tokenData.memory_limit = tokenData.user.tier === 'pro' ? null : 100;
296
+ }
297
+
298
+ return tokenData;
299
+ }
300
+
301
+ /**
302
+ * Refresh access token
303
+ */
304
+ async refreshToken(refreshToken) {
305
+ if (!refreshToken) {
306
+ throw new Error('No refresh token available');
307
+ }
308
+
309
+ console.log('πŸ”„ Refreshing authentication token...');
310
+
311
+ const response = await fetch(`${this.apiUrl}/api/auth/refresh`, {
312
+ method: 'POST',
313
+ headers: {
314
+ 'Content-Type': 'application/json',
315
+ 'User-Agent': 'purmemo-mcp/2.0.0'
316
+ },
317
+ body: JSON.stringify({
318
+ refresh_token: refreshToken
319
+ })
320
+ });
321
+
322
+ if (!response.ok) {
323
+ const error = await response.text();
324
+ throw new Error(`Token refresh failed: ${error}`);
325
+ }
326
+
327
+ const tokenData = await response.json();
328
+
329
+ // Add expiry time
330
+ if (tokenData.expires_in) {
331
+ tokenData.expires_at = new Date(Date.now() + tokenData.expires_in * 1000).toISOString();
332
+ }
333
+
334
+ // Store refreshed token
335
+ await this.tokenStore.saveToken(tokenData);
336
+
337
+ console.log('βœ… Token refreshed successfully');
338
+
339
+ return tokenData.access_token;
340
+ }
341
+
342
+ /**
343
+ * Clear stored authentication
344
+ */
345
+ async logout() {
346
+ await this.tokenStore.clearToken();
347
+ console.log('πŸ‘‹ Logged out successfully');
348
+ }
349
+
350
+ /**
351
+ * Generate PKCE code verifier
352
+ */
353
+ generateCodeVerifier() {
354
+ return crypto.randomBytes(32).toString('base64url');
355
+ }
356
+
357
+ /**
358
+ * Generate PKCE code challenge from verifier
359
+ */
360
+ generateCodeChallenge(verifier) {
361
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
362
+ }
363
+ }
364
+
365
+ export default OAuthManager;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Secure Token Storage for Purmemo MCP
3
+ * Stores OAuth tokens securely in user's home directory
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import crypto from 'crypto';
9
+ import os from 'os';
10
+
11
+ class TokenStore {
12
+ constructor() {
13
+ // Store tokens in user's home directory
14
+ this.configDir = path.join(os.homedir(), '.purmemo');
15
+ this.tokenFile = path.join(this.configDir, 'auth.json');
16
+ this.encryptionKey = this.getEncryptionKey();
17
+ }
18
+
19
+ /**
20
+ * Get or generate encryption key for token storage
21
+ */
22
+ getEncryptionKey() {
23
+ // Use machine ID + user info for key generation
24
+ const machineId = os.hostname() + os.userInfo().username;
25
+ return crypto.createHash('sha256').update(machineId).digest();
26
+ }
27
+
28
+ /**
29
+ * Ensure config directory exists
30
+ */
31
+ async ensureConfigDir() {
32
+ try {
33
+ await fs.mkdir(this.configDir, { recursive: true });
34
+ // Set restrictive permissions (owner read/write only)
35
+ if (process.platform !== 'win32') {
36
+ await fs.chmod(this.configDir, 0o700);
37
+ }
38
+ } catch (error) {
39
+ console.error('Failed to create config directory:', error);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Encrypt data
45
+ */
46
+ encrypt(data) {
47
+ const iv = crypto.randomBytes(16);
48
+ const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
49
+
50
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
51
+ encrypted += cipher.final('hex');
52
+
53
+ return {
54
+ iv: iv.toString('hex'),
55
+ data: encrypted
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Decrypt data
61
+ */
62
+ decrypt(encryptedData) {
63
+ const iv = Buffer.from(encryptedData.iv, 'hex');
64
+ const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
65
+
66
+ let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
67
+ decrypted += decipher.final('utf8');
68
+
69
+ return JSON.parse(decrypted);
70
+ }
71
+
72
+ /**
73
+ * Save token to disk
74
+ */
75
+ async saveToken(tokenData) {
76
+ await this.ensureConfigDir();
77
+
78
+ // Encrypt token data
79
+ const encrypted = this.encrypt(tokenData);
80
+
81
+ // Write to file
82
+ await fs.writeFile(
83
+ this.tokenFile,
84
+ JSON.stringify(encrypted, null, 2),
85
+ 'utf8'
86
+ );
87
+
88
+ // Set restrictive permissions
89
+ if (process.platform !== 'win32') {
90
+ await fs.chmod(this.tokenFile, 0o600);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get stored token
96
+ */
97
+ async getToken() {
98
+ try {
99
+ const data = await fs.readFile(this.tokenFile, 'utf8');
100
+ const encrypted = JSON.parse(data);
101
+ return this.decrypt(encrypted);
102
+ } catch (error) {
103
+ // File doesn't exist or is corrupted
104
+ if (error.code === 'ENOENT') {
105
+ return null;
106
+ }
107
+ console.error('Failed to read token:', error.message);
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Clear stored token
114
+ */
115
+ async clearToken() {
116
+ try {
117
+ await fs.unlink(this.tokenFile);
118
+ } catch (error) {
119
+ // Ignore if file doesn't exist
120
+ if (error.code !== 'ENOENT') {
121
+ console.error('Failed to clear token:', error);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check if token exists
128
+ */
129
+ async hasToken() {
130
+ try {
131
+ await fs.access(this.tokenFile);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Get user info from stored token
140
+ */
141
+ async getUserInfo() {
142
+ const token = await this.getToken();
143
+ if (!token) return null;
144
+
145
+ return {
146
+ user_id: token.user?.id,
147
+ email: token.user?.email,
148
+ tier: token.user_tier || 'free',
149
+ memory_limit: token.memory_limit,
150
+ expires_at: token.expires_at
151
+ };
152
+ }
153
+ }
154
+
155
+ export default TokenStore;
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pΕ«rmemo MCP Server v2.0.0 - With OAuth Support
4
+ * Seamless authentication without manual API key configuration
5
+ */
6
+
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema,
12
+ } from '@modelcontextprotocol/sdk/types.js';
13
+ import fetch from 'node-fetch';
14
+ import OAuthManager from './auth/oauth-manager.js';
15
+
16
+ // Initialize OAuth manager
17
+ const authManager = new OAuthManager({
18
+ apiUrl: process.env.PUO_MEMO_API_URL || 'https://api.purmemo.ai'
19
+ });
20
+
21
+ // API URL configuration
22
+ const API_URL = process.env.PUO_MEMO_API_URL || 'https://api.purmemo.ai';
23
+
24
+ // User state
25
+ let userInfo = null;
26
+ let memoryCount = 0;
27
+
28
+ // Tool definitions
29
+ const TOOLS = [
30
+ {
31
+ name: 'memory',
32
+ description: 'πŸ’Ύ Save anything to memory',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ content: { type: 'string', description: 'What to remember' },
37
+ title: { type: 'string', description: 'Optional: Title for the memory' },
38
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional: Tags' },
39
+ attachments: { type: 'array', items: { type: 'string' }, description: 'Optional: File paths or URLs' }
40
+ },
41
+ required: ['content']
42
+ }
43
+ },
44
+ {
45
+ name: 'recall',
46
+ description: 'πŸ” Search your memories',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ query: { type: 'string', description: 'What to search for' },
51
+ limit: { type: 'integer', description: 'How many results (default: 10)', default: 10 },
52
+ search_type: { type: 'string', enum: ['keyword', 'semantic', 'hybrid'], default: 'hybrid' }
53
+ },
54
+ required: ['query']
55
+ }
56
+ },
57
+ {
58
+ name: 'entities',
59
+ description: '🏷️ Extract entities from memories (people, places, concepts)',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ entity_name: { type: 'string', description: 'Optional: Specific entity to look up' },
64
+ entity_type: {
65
+ type: 'string',
66
+ enum: ['person', 'organization', 'location', 'concept', 'technology', 'project', 'document', 'event'],
67
+ description: 'Optional: Filter by entity type'
68
+ }
69
+ }
70
+ }
71
+ },
72
+ {
73
+ name: 'attach',
74
+ description: 'πŸ“Ž Attach files to an existing memory',
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ memory_id: { type: 'string', description: 'Memory ID to attach files to' },
79
+ file_paths: { type: 'array', items: { type: 'string' }, description: 'File paths or URLs' }
80
+ },
81
+ required: ['memory_id', 'file_paths']
82
+ }
83
+ },
84
+ {
85
+ name: 'correction',
86
+ description: '✏️ Add a correction to an existing memory',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ memory_id: { type: 'string', description: 'ID of the memory to correct' },
91
+ correction: { type: 'string', description: 'The corrected content' },
92
+ reason: { type: 'string', description: 'Optional: Reason for the correction' }
93
+ },
94
+ required: ['memory_id', 'correction']
95
+ }
96
+ }
97
+ ];
98
+
99
+ // Create server
100
+ const server = new Server(
101
+ {
102
+ name: 'purmemo-mcp',
103
+ version: '2.0.0'
104
+ },
105
+ {
106
+ capabilities: {
107
+ tools: {}
108
+ }
109
+ }
110
+ );
111
+
112
+ /**
113
+ * Get authentication token, prompting for OAuth if needed
114
+ */
115
+ async function getAuthToken() {
116
+ let token = await authManager.getToken();
117
+
118
+ if (!token) {
119
+ console.log('\n⚠️ Authentication required for Purmemo MCP');
120
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
121
+ console.log('This is a one-time setup. You\'ll be redirected to sign in.');
122
+ console.log('After authentication, all tools will work automatically.\n');
123
+
124
+ token = await authManager.authenticate();
125
+
126
+ // Get user info after authentication
127
+ userInfo = await authManager.tokenStore.getUserInfo();
128
+ if (userInfo) {
129
+ console.log(`πŸ‘€ Authenticated as: ${userInfo.email}`);
130
+ console.log(`πŸ“Š Account tier: ${userInfo.tier}`);
131
+ if (userInfo.memory_limit) {
132
+ console.log(`πŸ“ Memory limit: ${userInfo.memory_limit} (upgrade to Pro for unlimited)`);
133
+ }
134
+ console.log('');
135
+ }
136
+ }
137
+
138
+ return token;
139
+ }
140
+
141
+ /**
142
+ * Check if user has reached memory limit (for free tier)
143
+ */
144
+ async function checkMemoryLimit(token) {
145
+ if (!userInfo) {
146
+ userInfo = await authManager.tokenStore.getUserInfo();
147
+ }
148
+
149
+ if (userInfo && userInfo.tier === 'free' && userInfo.memory_limit) {
150
+ // Get current memory count
151
+ try {
152
+ const response = await fetch(`${API_URL}/api/v5/memories?limit=1`, {
153
+ headers: {
154
+ 'Authorization': `Bearer ${token}`,
155
+ 'User-Agent': 'purmemo-mcp/2.0.0'
156
+ }
157
+ });
158
+
159
+ if (response.ok) {
160
+ const data = await response.json();
161
+ memoryCount = data.total_count || 0;
162
+
163
+ if (memoryCount >= userInfo.memory_limit) {
164
+ return {
165
+ exceeded: true,
166
+ message: `You've reached your free tier limit of ${userInfo.memory_limit} memories. Upgrade to Pro at https://app.purmemo.ai for unlimited memories.`,
167
+ current: memoryCount,
168
+ limit: userInfo.memory_limit
169
+ };
170
+ }
171
+ }
172
+ } catch (error) {
173
+ console.error('Failed to check memory limit:', error);
174
+ }
175
+ }
176
+
177
+ return { exceeded: false };
178
+ }
179
+
180
+ // Handle tool listing
181
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
182
+ tools: TOOLS
183
+ }));
184
+
185
+ // Handle tool execution
186
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
+ const { name, arguments: args } = request.params;
188
+
189
+ try {
190
+ // Get auth token (will prompt for OAuth if needed)
191
+ const token = await getAuthToken();
192
+
193
+ if (!token) {
194
+ throw new Error('Authentication required. Please run the setup command to authenticate.');
195
+ }
196
+
197
+ // Check memory limit for memory creation
198
+ if (name === 'memory') {
199
+ const limitCheck = await checkMemoryLimit(token);
200
+ if (limitCheck.exceeded) {
201
+ return {
202
+ error: 'Memory limit exceeded',
203
+ message: limitCheck.message,
204
+ current_count: limitCheck.current,
205
+ limit: limitCheck.limit,
206
+ upgrade_url: 'https://app.purmemo.ai/upgrade'
207
+ };
208
+ }
209
+ }
210
+
211
+ let response;
212
+
213
+ switch (name) {
214
+ case 'memory':
215
+ // POST request for creating memory
216
+ response = await fetch(`${API_URL}/api/v5/memories`, {
217
+ method: 'POST',
218
+ headers: {
219
+ 'Authorization': `Bearer ${token}`,
220
+ 'Content-Type': 'application/json',
221
+ 'User-Agent': 'purmemo-mcp/2.0.0'
222
+ },
223
+ body: JSON.stringify(args)
224
+ });
225
+
226
+ // Increment memory count for free tier tracking
227
+ if (response.ok && userInfo?.tier === 'free') {
228
+ memoryCount++;
229
+ if (userInfo.memory_limit) {
230
+ const remaining = userInfo.memory_limit - memoryCount;
231
+ if (remaining > 0 && remaining <= 10) {
232
+ console.log(`ℹ️ ${remaining} memories remaining in free tier`);
233
+ }
234
+ }
235
+ }
236
+ break;
237
+
238
+ case 'recall':
239
+ // GET request for search
240
+ const searchParams = new URLSearchParams({
241
+ q: args.query || '',
242
+ limit: args.limit || 10,
243
+ search_type: args.search_type || 'hybrid'
244
+ });
245
+ response = await fetch(`${API_URL}/api/v5/memories/search?${searchParams}`, {
246
+ headers: {
247
+ 'Authorization': `Bearer ${token}`,
248
+ 'User-Agent': 'purmemo-mcp/2.0.0'
249
+ }
250
+ });
251
+ break;
252
+
253
+ case 'entities':
254
+ // GET request for entities
255
+ const entityParams = new URLSearchParams();
256
+ if (args.entity_name) entityParams.append('name', args.entity_name);
257
+ if (args.entity_type) entityParams.append('type', args.entity_type);
258
+
259
+ response = await fetch(`${API_URL}/api/v5/entities?${entityParams}`, {
260
+ headers: {
261
+ 'Authorization': `Bearer ${token}`,
262
+ 'User-Agent': 'purmemo-mcp/2.0.0'
263
+ }
264
+ });
265
+ break;
266
+
267
+ case 'attach':
268
+ // POST request for attachments
269
+ response = await fetch(`${API_URL}/api/v5/memories/${args.memory_id}/attachments`, {
270
+ method: 'POST',
271
+ headers: {
272
+ 'Authorization': `Bearer ${token}`,
273
+ 'Content-Type': 'application/json',
274
+ 'User-Agent': 'purmemo-mcp/2.0.0'
275
+ },
276
+ body: JSON.stringify({ file_paths: args.file_paths })
277
+ });
278
+ break;
279
+
280
+ case 'correction':
281
+ // POST request for corrections
282
+ response = await fetch(`${API_URL}/api/v5/memories/${args.memory_id}/corrections`, {
283
+ method: 'POST',
284
+ headers: {
285
+ 'Authorization': `Bearer ${token}`,
286
+ 'Content-Type': 'application/json',
287
+ 'User-Agent': 'purmemo-mcp/2.0.0'
288
+ },
289
+ body: JSON.stringify({
290
+ correction: args.correction,
291
+ reason: args.reason
292
+ })
293
+ });
294
+ break;
295
+
296
+ default:
297
+ throw new Error(`Unknown tool: ${name}`);
298
+ }
299
+
300
+ if (!response.ok) {
301
+ const error = await response.text();
302
+
303
+ // Handle specific error cases
304
+ if (response.status === 401) {
305
+ // Token might be expired, try to refresh
306
+ console.log('πŸ”„ Authentication expired, refreshing...');
307
+ await authManager.tokenStore.clearToken();
308
+ const newToken = await authManager.authenticate();
309
+
310
+ // Suggest retrying the operation
311
+ throw new Error('Authentication refreshed. Please try your request again.');
312
+ }
313
+
314
+ throw new Error(`API error: ${error}`);
315
+ }
316
+
317
+ return await response.json();
318
+
319
+ } catch (error) {
320
+ console.error(`❌ Error executing ${name}:`, error.message);
321
+
322
+ // Provide helpful error messages
323
+ if (error.message.includes('ECONNREFUSED')) {
324
+ throw new Error('Unable to connect to Purmemo API. Please check your internet connection.');
325
+ }
326
+
327
+ if (error.message.includes('Authentication')) {
328
+ throw new Error(`Authentication issue: ${error.message}. You may need to reconnect your account.`);
329
+ }
330
+
331
+ throw error;
332
+ }
333
+ });
334
+
335
+ // Initialize on startup
336
+ async function initialize() {
337
+ console.log('🧠 pūrmemo MCP v2.0.0 starting...\n');
338
+
339
+ // Check if we have stored authentication
340
+ const hasAuth = await authManager.tokenStore.hasToken();
341
+
342
+ if (hasAuth) {
343
+ userInfo = await authManager.tokenStore.getUserInfo();
344
+ if (userInfo) {
345
+ console.log(`βœ… Authenticated as: ${userInfo.email}`);
346
+ console.log(`πŸ“Š Account tier: ${userInfo.tier}`);
347
+ if (userInfo.memory_limit) {
348
+ console.log(`πŸ“ Memory limit: ${userInfo.memory_limit}`);
349
+ }
350
+ }
351
+ } else {
352
+ // Check for legacy API key
353
+ if (process.env.PUO_MEMO_API_KEY) {
354
+ console.log('πŸ“” Using API key from environment (legacy mode)');
355
+ console.log('πŸ’‘ Tip: Remove PUO_MEMO_API_KEY to use OAuth authentication');
356
+ } else {
357
+ console.log('πŸ” No authentication found');
358
+ console.log('πŸ“± You\'ll be prompted to sign in when you use your first tool');
359
+ console.log(' This is a one-time setup for seamless access');
360
+ }
361
+ }
362
+
363
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
364
+ console.log('Ready to serve MCP requests\n');
365
+ }
366
+
367
+ // Start server
368
+ initialize().then(() => {
369
+ const transport = new StdioServerTransport();
370
+ server.connect(transport);
371
+ }).catch(error => {
372
+ console.error('Failed to initialize:', error);
373
+ process.exit(1);
374
+ });
package/src/setup.js ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pΕ«rmemo MCP Setup Command
4
+ * Allows users to manually authenticate or manage their connection
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import chalk from 'chalk';
9
+ import inquirer from 'inquirer';
10
+ import OAuthManager from './auth/oauth-manager.js';
11
+ import TokenStore from './auth/token-store.js';
12
+ import ora from 'ora';
13
+
14
+ const program = new Command();
15
+ const authManager = new OAuthManager();
16
+ const tokenStore = new TokenStore();
17
+
18
+ // ASCII Art Banner
19
+ const banner = `
20
+ ╔═══════════════════════════════════════════╗
21
+ β•‘ β•‘
22
+ β•‘ 🧠 pΕ«rmemo MCP β•‘
23
+ β•‘ Memory Management Tool β•‘
24
+ β•‘ β•‘
25
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
26
+ `;
27
+
28
+ program
29
+ .name('purmemo-mcp')
30
+ .description('Setup and manage your pΕ«rmemo MCP connection')
31
+ .version('2.0.0');
32
+
33
+ program
34
+ .command('setup')
35
+ .description('Connect your pΕ«rmemo account')
36
+ .action(async () => {
37
+ console.log(chalk.cyan(banner));
38
+
39
+ // Check if already authenticated
40
+ const hasAuth = await tokenStore.hasToken();
41
+
42
+ if (hasAuth) {
43
+ const userInfo = await tokenStore.getUserInfo();
44
+ console.log(chalk.green('βœ… You are already authenticated!'));
45
+ console.log(chalk.gray(` Email: ${userInfo?.email}`));
46
+ console.log(chalk.gray(` Tier: ${userInfo?.tier}`));
47
+ console.log('');
48
+
49
+ const { action } = await inquirer.prompt([
50
+ {
51
+ type: 'list',
52
+ name: 'action',
53
+ message: 'What would you like to do?',
54
+ choices: [
55
+ { name: 'Keep current connection', value: 'keep' },
56
+ { name: 'Sign in with a different account', value: 'reconnect' },
57
+ { name: 'Sign out', value: 'logout' }
58
+ ]
59
+ }
60
+ ]);
61
+
62
+ if (action === 'keep') {
63
+ console.log(chalk.green('\n✨ Your connection is active and ready to use!'));
64
+ process.exit(0);
65
+ } else if (action === 'logout') {
66
+ await authManager.logout();
67
+ console.log(chalk.yellow('\nπŸ‘‹ You have been signed out.'));
68
+ process.exit(0);
69
+ }
70
+ // Fall through to reconnect
71
+ }
72
+
73
+ console.log(chalk.yellow('\nπŸ“± Starting authentication process...\n'));
74
+ console.log(chalk.gray('You will be redirected to your browser to sign in.'));
75
+ console.log(chalk.gray('Choose your preferred method: Google, GitHub, or Email.\n'));
76
+
77
+ const spinner = ora('Preparing authentication...').start();
78
+
79
+ try {
80
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Brief pause
81
+ spinner.stop();
82
+
83
+ const token = await authManager.authenticate();
84
+
85
+ if (token) {
86
+ const userInfo = await tokenStore.getUserInfo();
87
+
88
+ console.log('');
89
+ console.log(chalk.green.bold('πŸŽ‰ Success! You are now connected to pΕ«rmemo'));
90
+ console.log('');
91
+ console.log(chalk.white('Account Information:'));
92
+ console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
93
+ console.log(chalk.cyan(` Email: ${userInfo?.email}`));
94
+ console.log(chalk.cyan(` Tier: ${userInfo?.tier === 'pro' ? '⭐ Pro' : 'πŸ†“ Free'}`));
95
+
96
+ if (userInfo?.memory_limit) {
97
+ console.log(chalk.yellow(` Memory Limit: ${userInfo.memory_limit} memories`));
98
+ console.log(chalk.gray(` (Upgrade to Pro for unlimited memories)`));
99
+ } else {
100
+ console.log(chalk.green(` Memory Limit: Unlimited`));
101
+ }
102
+
103
+ console.log('');
104
+ console.log(chalk.white('Available Tools:'));
105
+ console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
106
+ console.log(chalk.green(' βœ“ memory ') + chalk.gray('- Save anything to memory'));
107
+ console.log(chalk.green(' βœ“ recall ') + chalk.gray('- Search your memories'));
108
+ console.log(chalk.green(' βœ“ entities ') + chalk.gray('- Extract people, places, concepts'));
109
+ console.log(chalk.green(' βœ“ attach ') + chalk.gray('- Attach files to memories'));
110
+ console.log(chalk.green(' βœ“ correction') + chalk.gray('- Correct existing memories'));
111
+
112
+ console.log('');
113
+ console.log(chalk.cyan.bold('πŸš€ You can now use all pΕ«rmemo tools in Claude Desktop!'));
114
+ console.log('');
115
+ }
116
+ } catch (error) {
117
+ spinner.fail('Authentication failed');
118
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
119
+ console.log(chalk.gray('\nPlease try again or visit https://app.purmemo.ai for help.'));
120
+ process.exit(1);
121
+ }
122
+ });
123
+
124
+ program
125
+ .command('status')
126
+ .description('Check your connection status')
127
+ .action(async () => {
128
+ console.log(chalk.cyan(banner));
129
+
130
+ const hasAuth = await tokenStore.hasToken();
131
+
132
+ if (!hasAuth) {
133
+ console.log(chalk.yellow('⚠️ Not authenticated'));
134
+ console.log(chalk.gray('\nRun "npx purmemo-mcp setup" to connect your account.'));
135
+ return;
136
+ }
137
+
138
+ const userInfo = await tokenStore.getUserInfo();
139
+ const spinner = ora('Checking connection...').start();
140
+
141
+ try {
142
+ // Test API connection
143
+ const token = await authManager.getToken();
144
+ const response = await fetch('https://api.purmemo.ai/api/v5/memories?limit=1', {
145
+ headers: {
146
+ 'Authorization': `Bearer ${token}`,
147
+ 'User-Agent': 'purmemo-mcp/2.0.0'
148
+ }
149
+ });
150
+
151
+ spinner.stop();
152
+
153
+ if (response.ok) {
154
+ const data = await response.json();
155
+ const memoryCount = data.total_count || 0;
156
+
157
+ console.log(chalk.green.bold('βœ… Connection Active'));
158
+ console.log('');
159
+ console.log(chalk.white('Account Details:'));
160
+ console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
161
+ console.log(` Email: ${userInfo?.email}`);
162
+ console.log(` Tier: ${userInfo?.tier === 'pro' ? '⭐ Pro' : 'πŸ†“ Free'}`);
163
+ console.log(` Memories: ${memoryCount}`);
164
+
165
+ if (userInfo?.memory_limit) {
166
+ const remaining = userInfo.memory_limit - memoryCount;
167
+ console.log(` Remaining: ${remaining} of ${userInfo.memory_limit}`);
168
+
169
+ if (remaining <= 10) {
170
+ console.log('');
171
+ console.log(chalk.yellow('⚠️ You are approaching your free tier limit.'));
172
+ console.log(chalk.gray(' Upgrade to Pro at https://app.purmemo.ai'));
173
+ }
174
+ }
175
+
176
+ // Check token expiry
177
+ if (userInfo?.expires_at) {
178
+ const expiresAt = new Date(userInfo.expires_at);
179
+ const now = new Date();
180
+ const hoursUntilExpiry = Math.floor((expiresAt - now) / (1000 * 60 * 60));
181
+
182
+ if (hoursUntilExpiry < 24) {
183
+ console.log('');
184
+ console.log(chalk.yellow(`⚠️ Token expires in ${hoursUntilExpiry} hours`));
185
+ console.log(chalk.gray(' It will auto-refresh when needed.'));
186
+ }
187
+ }
188
+ } else {
189
+ spinner.fail('Connection test failed');
190
+ console.log(chalk.red(`\n❌ API returned status: ${response.status}`));
191
+ console.log(chalk.gray('\nTry running "npx purmemo-mcp setup" to reconnect.'));
192
+ }
193
+ } catch (error) {
194
+ spinner.fail('Connection test failed');
195
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
196
+ console.log(chalk.gray('\nCheck your internet connection and try again.'));
197
+ }
198
+ });
199
+
200
+ program
201
+ .command('logout')
202
+ .description('Sign out from your pΕ«rmemo account')
203
+ .action(async () => {
204
+ console.log(chalk.cyan(banner));
205
+
206
+ const hasAuth = await tokenStore.hasToken();
207
+
208
+ if (!hasAuth) {
209
+ console.log(chalk.yellow('⚠️ You are not signed in.'));
210
+ return;
211
+ }
212
+
213
+ const userInfo = await tokenStore.getUserInfo();
214
+
215
+ const { confirm } = await inquirer.prompt([
216
+ {
217
+ type: 'confirm',
218
+ name: 'confirm',
219
+ message: `Are you sure you want to sign out from ${userInfo?.email}?`,
220
+ default: false
221
+ }
222
+ ]);
223
+
224
+ if (confirm) {
225
+ await authManager.logout();
226
+ console.log(chalk.green('\nβœ… Successfully signed out.'));
227
+ console.log(chalk.gray('Run "npx purmemo-mcp setup" to sign in again.'));
228
+ } else {
229
+ console.log(chalk.gray('\nSign out cancelled.'));
230
+ }
231
+ });
232
+
233
+ program
234
+ .command('upgrade')
235
+ .description('Open upgrade page to get Pro features')
236
+ .action(async () => {
237
+ console.log(chalk.cyan(banner));
238
+ console.log(chalk.yellow('πŸš€ Opening upgrade page...'));
239
+ console.log(chalk.gray('\nPro features include:'));
240
+ console.log(chalk.green(' β€’ Unlimited memories'));
241
+ console.log(chalk.green(' β€’ Advanced AI models'));
242
+ console.log(chalk.green(' β€’ Priority support'));
243
+ console.log(chalk.green(' β€’ API access'));
244
+
245
+ const open = (await import('open')).default;
246
+ await open('https://app.purmemo.ai/upgrade');
247
+ });
248
+
249
+ // Handle unknown commands
250
+ program.on('command:*', () => {
251
+ console.error(chalk.red('\nInvalid command: %s'), program.args.join(' '));
252
+ console.log(chalk.gray('Run "npx purmemo-mcp --help" for available commands.'));
253
+ process.exit(1);
254
+ });
255
+
256
+ // Show help if no command provided
257
+ if (!process.argv.slice(2).length) {
258
+ console.log(chalk.cyan(banner));
259
+ console.log(chalk.white('Welcome to pΕ«rmemo MCP!\n'));
260
+ console.log(chalk.gray('Get started by connecting your account:\n'));
261
+ console.log(chalk.cyan(' npx purmemo-mcp setup\n'));
262
+ console.log(chalk.gray('For more commands, run: npx purmemo-mcp --help'));
263
+ }
264
+
265
+ program.parse();