persyst-mcp 1.0.1 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zayn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Local-first MCP memory server for coding agents.**
4
4
 
5
- Persyst gives AI coding agents (Claude Code, Cursor, Aider, Windsurf) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — no cloud, no API keys, works offline.
5
+ Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider, Windsurf, Antigravity) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — no cloud, no API keys, works offline.
6
6
 
7
7
  ## How It Works
8
8
 
@@ -14,59 +14,96 @@ Your AI Agent ←→ MCP (stdio) ←→ Persyst ←→ SQLite (local)
14
14
  2. **Agent searches memories** → Persyst finds matches by both keywords AND meaning
15
15
  3. **"dark mode" ↔ "night theme"** → Semantic search understands synonyms
16
16
 
17
- ## Quick Start
17
+ > 🚨 **First-Run Note**: On the first start, Persyst will automatically download the local embedding model (`all-MiniLM-L6-v2` ~50MB). This can take 30-60 seconds depending on your connection. The server will log `Loading embedding model...` and then proceed normally.
18
18
 
19
- ### 1. Install
19
+ ---
20
20
 
21
- ```bash
22
- npm install -g persyst-mcp
23
- ```
21
+ ## Quick Start
24
22
 
25
- ### 2. Add to Claude Code
23
+ You don't need to install anything globally. You can run it instantly using `npx`:
26
24
 
27
- Edit your Claude Code MCP config (`claude_desktop_config.json`):
25
+ ### 1. Add to Claude Code or Claude Desktop
28
26
 
27
+ #### Claude Code (CLI)
28
+ Add this to your global configuration file located at `~/.claude.json`:
29
29
  ```json
30
30
  {
31
31
  "mcpServers": {
32
32
  "persyst": {
33
- "command": "persyst-mcp"
33
+ "command": "npx",
34
+ "args": ["-y", "persyst-mcp"]
34
35
  }
35
36
  }
36
37
  }
37
38
  ```
38
39
 
39
- ### 3. Use It
40
-
41
- In Claude Code, the agent can now call tools like:
42
- - `add_memory` — Store a fact
43
- - `search_memories` — Find relevant memories
44
- - `get_memory` — Get a specific memory
45
- - `update_memory` — Update a memory
46
- - `delete_memory` — Remove a memory
47
- - `get_recent_memories` — Latest memories
48
- - `get_important_memories` — Most important memories
40
+ #### Claude Desktop
41
+ Add this to your Claude Desktop configuration file:
42
+ * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
43
+ * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
44
+ * **Linux**: `~/.config/Claude/claude_desktop_config.json`
49
45
 
50
- ## Setup for Other Agents
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "persyst": {
50
+ "command": "npx",
51
+ "args": ["-y", "persyst-mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
51
56
 
52
- ### Cursor
57
+ ---
53
58
 
54
- Add to your Cursor MCP settings:
59
+ ## Setup for Other Agents
55
60
 
61
+ ### VS Code (Cline / Roo Code)
62
+ Add this configuration to your user settings under the MCP settings file (`cline_mcp_settings.json`):
56
63
  ```json
57
64
  {
58
- "persyst": {
59
- "command": "persyst-mcp"
65
+ "mcpServers": {
66
+ "persyst": {
67
+ "command": "npx",
68
+ "args": ["-y", "persyst-mcp"]
69
+ }
60
70
  }
61
71
  }
62
72
  ```
63
73
 
64
- ### Aider
74
+ ### Cursor
75
+ Add Persyst in Cursor under **Settings → Features → MCP**:
76
+ 1. Click **+ Add New MCP Server**
77
+ 2. Name: `persyst`
78
+ 3. Type: `stdio`
79
+ 4. Command: `npx -y persyst-mcp`
65
80
 
81
+ ### Aider
82
+ Start Aider from the command line passing the server command:
66
83
  ```bash
67
- # Start the MCP server alongside Aider
68
- persyst-mcp &
84
+ aider --mcp-server persyst:npx -y persyst-mcp
69
85
  ```
86
+ Or append this to your `.aider.conf.yml` project file:
87
+ ```yaml
88
+ mcp-server:
89
+ - name: persyst
90
+ command: npx -y persyst-mcp
91
+ ```
92
+
93
+ ### Antigravity
94
+ Add Persyst to your Antigravity agent configuration file at `~/.gemini/antigravity/mcp_config.json`:
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "persyst": {
99
+ "command": "npx",
100
+ "args": ["-y", "persyst-mcp"]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ---
70
107
 
71
108
  ## Available Tools
72
109
 
@@ -76,10 +113,12 @@ persyst-mcp &
76
113
  | `search_memories` | Hybrid keyword + semantic search | `query` (string), `limit` (number) |
77
114
  | `get_memory` | Get memory by ID | `id` (number) |
78
115
  | `update_memory` | Update memory content | `id` (number), `content` (string) |
79
- | `delete_memory` | Delete a memory | `id` (number) |
116
+ | `delete_memory` | Delete a memory and clean up edges | `id` (number) |
80
117
  | `get_recent_memories` | Get latest memories | `limit` (number) |
81
118
  | `get_important_memories` | Get by importance score | `limit` (number) |
82
119
 
120
+ ---
121
+
83
122
  ## How Search Works
84
123
 
85
124
  Persyst uses **hybrid search** — combining two strategies:
@@ -89,28 +128,7 @@ Persyst uses **hybrid search** — combining two strategies:
89
128
 
90
129
  Results from both are merged. Keyword matches get a score boost so exact matches rank higher, but semantic matches still surface related memories.
91
130
 
92
- ## Architecture
93
-
94
- ```
95
- persyst/
96
- ├── index.js ← Entry point (starts MCP server)
97
- ├── src/
98
- │ ├── server.js ← MCP server (stdio transport)
99
- │ ├── database.js ← SQLite + schema + CRUD
100
- │ ├── search.js ← Hybrid search engine
101
- │ ├── embeddings.js ← Local embedding generation
102
- │ └── tools.js ← 7 MCP tool definitions
103
- ├── test/
104
- │ └── smoke.js ← End-to-end test
105
- └── db/ ← Database files (gitignored)
106
- ```
107
-
108
- ## Data Storage
109
-
110
- - Database location: `~/.persyst/persyst.db`
111
- - All data stays on your machine
112
- - No telemetry, no cloud calls, no API keys
113
- - Works offline (airplane mode ✓)
131
+ ---
114
132
 
115
133
  ## Tech Stack
116
134
 
@@ -121,21 +139,26 @@ persyst/
121
139
  - **Embeddings:** @huggingface/transformers + all-MiniLM-L6-v2 (384-dim, ~50MB)
122
140
  - **Protocol:** MCP over stdio
123
141
 
124
- ## Development
142
+ ---
125
143
 
126
- ```bash
127
- # Clone and install
128
- git clone <repo-url>
129
- cd persyst
130
- npm install
144
+ ## Troubleshooting
131
145
 
132
- # Run smoke test
133
- npm test
146
+ #### `better-sqlite3` installation fails
147
+ `better-sqlite3` compiles native C++ code on installation. Make sure you have python and C++ build tools installed on your system:
148
+ * **Windows:** Run `npm install --global windows-build-tools` or install Visual Studio Build Tools.
149
+ * **macOS/Linux:** Run `xcode-select --install` or install `build-essential`.
134
150
 
135
- # Start server directly
136
- node index.js
137
- ```
151
+ #### The agent is stuck or loading forever on startup
152
+ This is normal on the **very first run** because Persyst is downloading the ~50MB embedding model. Wait 30-60 seconds for it to complete. The next runs will be instant.
153
+
154
+ #### Command not found: `persyst-mcp`
155
+ Instead of running it globally, prefer using the `npx -y persyst-mcp` command in your agent configurations. It automatically installs and updates the server non-interactively.
156
+
157
+ #### Permission Denied
158
+ Do not run `npx` with `sudo`. If you run into permission issues, ensure your npm global prefix is owned by your user account.
159
+
160
+ ---
138
161
 
139
162
  ## License
140
163
 
141
- MIT
164
+ MIT License. See [LICENSE](LICENSE) for details.
package/index.js CHANGED
@@ -14,4 +14,7 @@
14
14
 
15
15
  import { startServer } from './src/server.js';
16
16
 
17
- startServer();
17
+ await startServer().catch(err => {
18
+ console.error('❌ Persyst failed to start:', err.message);
19
+ process.exit(1);
20
+ });
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "persyst-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "persyst-mcp": "index.js"
9
9
  },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "src/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
10
19
  "scripts": {
11
20
  "start": "node index.js",
12
21
  "test": "node test/smoke.js",
@@ -20,7 +29,12 @@
20
29
  "ai",
21
30
  "agents",
22
31
  "semantic-search",
23
- "knowledge-graph"
32
+ "knowledge-graph",
33
+ "claude-code",
34
+ "cursor",
35
+ "aider",
36
+ "windsurf",
37
+ "memory-server"
24
38
  ],
25
39
  "author": "Zayn",
26
40
  "license": "MIT",
@@ -0,0 +1,207 @@
1
+ /**
2
+ * attestation.js — Cryptographic Attestation Engine
3
+ *
4
+ * Implements Ed25519 signature generation and verification for search queries.
5
+ * Chains each attestation by linking to the hash of the previous one.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+ import db, { getLastAttestation, insertAttestation, getAttestationById } from './database.js';
13
+
14
+ const KEYS_DIR = join(homedir(), '.persyst', 'keys');
15
+
16
+ /**
17
+ * Initialize keypair if it doesn't already exist.
18
+ */
19
+ export function initializeKeys() {
20
+ mkdirSync(KEYS_DIR, { recursive: true });
21
+ const pubPath = join(KEYS_DIR, 'public.pem');
22
+ const privPath = join(KEYS_DIR, 'private.pem');
23
+
24
+ if (!existsSync(pubPath) || !existsSync(privPath)) {
25
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
26
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
27
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
28
+ });
29
+ writeFileSync(pubPath, publicKey);
30
+ writeFileSync(privPath, privateKey);
31
+ console.error('[persyst] Generated new Ed25519 keypair for attestation');
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Read private key.
37
+ */
38
+ function getPrivateKey() {
39
+ const privPath = join(KEYS_DIR, 'private.pem');
40
+ return readFileSync(privPath, 'utf8');
41
+ }
42
+
43
+ /**
44
+ * Read public key.
45
+ */
46
+ export function getPublicKey() {
47
+ const pubPath = join(KEYS_DIR, 'public.pem');
48
+ return readFileSync(pubPath, 'utf8');
49
+ }
50
+
51
+ /**
52
+ * Generate a new attestation for search results.
53
+ */
54
+ export function createAttestation(query, memories, agentId = null, sessionId = null) {
55
+ initializeKeys();
56
+
57
+ const attestationId = crypto.randomUUID();
58
+ const timestamp = new Date().toISOString();
59
+
60
+ // Map memories to {id, content_hash, score}
61
+ const memoriesRetrieved = memories.map(m => {
62
+ const contentHash = crypto.createHash('sha256').update(m.content).digest('hex');
63
+ return {
64
+ id: m.id,
65
+ content_hash: contentHash,
66
+ score: parseFloat(m.hybrid_score || 0)
67
+ };
68
+ });
69
+
70
+ // Fetch previous attestation hash for the hash chain
71
+ const lastAtt = getLastAttestation();
72
+ const previousHash = lastAtt ? lastAtt.hash : null;
73
+
74
+ // Construct document to sign (ordered keys to ensure canonical serialization)
75
+ const doc = {
76
+ attestation_id: attestationId,
77
+ query,
78
+ timestamp,
79
+ memories_retrieved: memoriesRetrieved,
80
+ agent_id: agentId || null,
81
+ session_id: sessionId || null,
82
+ previous_hash: previousHash
83
+ };
84
+
85
+ const dataToSign = JSON.stringify(doc);
86
+
87
+ // Sign document using Ed25519
88
+ const privateKey = getPrivateKey();
89
+ const signature = crypto.sign(null, Buffer.from(dataToSign), {
90
+ key: privateKey,
91
+ type: 'pkcs8',
92
+ format: 'pem'
93
+ }).toString('hex');
94
+
95
+ // Construct full record and compute its hash
96
+ const fullAttestation = {
97
+ ...doc,
98
+ signature
99
+ };
100
+
101
+ const hash = crypto.createHash('sha256').update(JSON.stringify(fullAttestation)).digest('hex');
102
+
103
+ const record = {
104
+ ...fullAttestation,
105
+ hash
106
+ };
107
+
108
+ // Persist to DB
109
+ insertAttestation(record);
110
+
111
+ return record;
112
+ }
113
+
114
+ /**
115
+ * Verify a single attestation record's signature and hash.
116
+ */
117
+ export function verifyAttestationRecord(attestation) {
118
+ try {
119
+ const doc = {
120
+ attestation_id: attestation.attestation_id,
121
+ query: attestation.query,
122
+ timestamp: attestation.timestamp,
123
+ memories_retrieved: typeof attestation.memories_retrieved === 'string'
124
+ ? JSON.parse(attestation.memories_retrieved)
125
+ : attestation.memories_retrieved,
126
+ agent_id: attestation.agent_id || null,
127
+ session_id: attestation.session_id || null,
128
+ previous_hash: attestation.previous_hash || null
129
+ };
130
+
131
+ const dataToSign = JSON.stringify(doc);
132
+ const publicKey = getPublicKey();
133
+
134
+ // Verify signature
135
+ const isSignatureValid = crypto.verify(
136
+ null,
137
+ Buffer.from(dataToSign),
138
+ {
139
+ key: publicKey,
140
+ type: 'spki',
141
+ format: 'pem'
142
+ },
143
+ Buffer.from(attestation.signature, 'hex')
144
+ );
145
+
146
+ if (!isSignatureValid) {
147
+ return { valid: false, error: 'Signature verification failed' };
148
+ }
149
+
150
+ // Verify hash integrity
151
+ const fullRecord = {
152
+ ...doc,
153
+ signature: attestation.signature
154
+ };
155
+ const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
156
+
157
+ if (computedHash !== attestation.hash) {
158
+ return { valid: false, error: 'Attestation hash mismatch' };
159
+ }
160
+
161
+ return { valid: true };
162
+ } catch (err) {
163
+ return { valid: false, error: err.message };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Verifies signature and chain integrity.
169
+ */
170
+ export function verifyChainIntegrity(attestationId) {
171
+ const att = getAttestationById(attestationId);
172
+ if (!att) {
173
+ return { valid: false, error: `Attestation not found: ${attestationId}` };
174
+ }
175
+
176
+ const selfVerify = verifyAttestationRecord(att);
177
+ if (!selfVerify.valid) {
178
+ return selfVerify;
179
+ }
180
+
181
+ // If there's a previous link, check it
182
+ if (att.previous_hash) {
183
+ const db = getLastAttestation(); // Wait, we can run raw query on DB or use prepared stmt
184
+ const prevAtt = getAttestationByHash(att.previous_hash);
185
+ if (!prevAtt) {
186
+ return { valid: false, error: `Broken chain: Previous attestation with hash ${att.previous_hash} not found` };
187
+ }
188
+
189
+ if (prevAtt.id >= att.id) {
190
+ return { valid: false, error: `Broken chain: Invalid sequence order` };
191
+ }
192
+
193
+ const prevVerify = verifyAttestationRecord(prevAtt);
194
+ if (!prevVerify.valid) {
195
+ return { valid: false, error: `Broken chain: Previous link is invalid: ${prevVerify.error}` };
196
+ }
197
+ }
198
+
199
+ return { valid: true, attestation: att };
200
+ }
201
+
202
+ /**
203
+ * Helper to fetch attestation by hash since it's not exposed globally.
204
+ */
205
+ function getAttestationByHash(hash) {
206
+ return db.prepare('SELECT * FROM attestations WHERE hash = ?').get(hash) || null;
207
+ }