@postnesia/mcp 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -1,117 +1,63 @@
1
- # Postnesia Memory MCP Server
1
+ # @postnesia/mcp
2
2
 
3
- Model Context Protocol server for main agent memory system. Exposes memory operations as standardized tools.
3
+ MCP server for the Postnesia memory system. Exposes all memory, journal, and task operations as Model Context Protocol tools over stdio.
4
4
 
5
- ## Installation
5
+ ## Usage
6
6
 
7
- ```bash
8
- cd postnesia
9
- npm install
10
- npm run db:generate
11
- ```
12
-
13
- ## Configuration
14
-
15
- Add to your MCP settings (e.g., Claude Desktop config):
7
+ Add to your MCP config (e.g. `.claude/settings.json` or Claude Desktop):
16
8
 
17
9
  ```json
18
10
  {
19
11
  "mcpServers": {
20
12
  "postnesia": {
21
- "command": "/absolute/path/to/tsx",
13
+ "command": "npx",
14
+ "args": ["postnesia-mcp"],
22
15
  "env": {
23
- "DATABASE_URL": "file:/absolute/path/to/memory.db",
24
- "GEMINI_API_KEY": "token-for-gemini-embedding-model-api"
16
+ "DATABASE_URL": "file:///absolute/path/to/memory.db",
17
+ "GEMINI_API_KEY": "your-gemini-api-key",
18
+ "EMBEDDING_MODEL": "gemini-embedding-001",
19
+ "EMBEDDING_DIMENSIONS": "768"
25
20
  }
26
21
  }
27
22
  }
28
23
  }
29
24
  ```
30
25
 
31
- Or use npm script:
26
+ `DATABASE_URL` must be an absolute `file://` URL.
32
27
 
33
- ```json
34
- {
35
- "mcpServers": {
36
- "openmind": {
37
- "command": "/absolute/path/to/tsx",
38
- "env": {
39
- "DATABASE_URL": "/absolute/path/to/memory.db",
40
- "GEMINI_API_KEY": "token-for-gemini-embedding-model-api"
41
- }
42
- }
43
- }
44
- }
45
- ```
46
-
47
- ## Available Tools
48
-
49
- ### memory_search
50
- Semantic search across memories.
51
- - `query` (required): Search text
52
- - `maxResults` (optional): Max results (default: 10)
53
- - `minScore` (optional): Min similarity 0-1 (default: 0.3)
54
-
55
- ### memory_add
56
- Create a new memory.
57
- - `content` (required): Full memory text
58
- - `contentL1` (optional): Compressed form
59
- - `type` (required): event|decision|lesson|preference|person|technical
60
- - `importance` (required): 1-5
61
- - `tags` (required): Array of tags
62
- - `context` (optional): Creation context
63
-
64
- ### memory_recent
65
- Get recent memories.
66
- - `hours` (optional): Hours to look back (default: 24)
67
- - `limit` (optional): Max results (default: 20)
28
+ ## Tools
68
29
 
69
- ### memory_context
70
- Get contextually related memories.
71
- - `query` (required): Context query
72
- - `maxResults` (optional): Max results (default: 5)
30
+ ### Memory
73
31
 
74
- ### memory_stats
75
- Get database statistics. No parameters.
32
+ | Tool | Required | Optional |
33
+ |---|---|---|
34
+ | `memory_search` | `query` | `limit` (default: 10) |
35
+ | `memory_add` | `content`, `type`, `importance`, `tags` | `contentL1`, `context`, `core` |
36
+ | `memory_update_core` | `memoryId`, `content`, `contentL1` | — |
37
+ | `memory_recent` | — | `hours` (default: 24), `limit` (default: 20) |
38
+ | `memory_stats` | — | — |
39
+ | `memory_consolidate` | — | — |
40
+ | `memory_relationships` | `memoryId` | — |
76
41
 
77
- ### memory_consolidate
78
- Run consolidation cycle (decay + boost). No parameters - always applies changes.
42
+ **Memory types:** `event` `decision` `lesson` `preference` `person` `technical`
79
43
 
80
- ### journal_add
81
- Add daily journal entry.
82
- - `date` (required): YYYY-MM-DD
83
- - `content` (required): Full narrative
84
- - `learned` (optional): What I learned
85
- - `learnedAboutRye` (optional): What I learned about Rye
86
- - `keyMoments` (optional): Key moments
87
- - `mood` (optional): Mood/feeling
44
+ ### Journal
88
45
 
89
- ### journal_recent
90
- Get recent journal entries.
91
- - `days` (optional): Days to look back (default: 7)
46
+ | Tool | Required | Optional |
47
+ |---|---|---|
48
+ | `journal_add` | `date` (YYYY-MM-DD), `content` | `learned`, `learnedAboutRye`, `keyMoments`, `mood` |
49
+ | `journal_recent` | — | `days` (default: 7) |
92
50
 
93
- ### memory_relationships
94
- View relationship graph for a memory.
95
- - `memoryId` (required): Memory ID to explore
51
+ ### Tasks
96
52
 
97
- ## Access Tracking
98
-
99
- All read operations (search, recent, context) automatically log accesses, which feed into the importance dynamics system.
100
-
101
- ## Testing
102
-
103
- Test the server manually:
104
-
105
- ```bash
106
- npm run mcp
107
- ```
53
+ | Tool | Required | Optional |
54
+ |---|---|---|
55
+ | `task_create` | `title` | `description`, `session_id`, `memory_id` |
56
+ | `task_update` | `taskId` | `status`, `title`, `description` |
57
+ | `task_list` | — | `status`, `session_id`, `limit` (default: 50) |
108
58
 
109
- Then send MCP requests via stdin (see MCP protocol docs).
59
+ **Task statuses:** `pending` `in_progress` `completed` `cancelled`
110
60
 
111
- ## Architecture
61
+ ## Implementation
112
62
 
113
- - Built on Prisma + SQLite
114
- - Automatic access tracking
115
- - Dynamic importance scoring (decay + boost)
116
- - Relationship graph support
117
- - Journal integration
63
+ Built with `McpServer` from `@modelcontextprotocol/sdk` and Zod input schemas. All read operations (`memory_search`, `memory_recent`) automatically log accesses which feed into the importance decay/boost system.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MCP Server for Belle's Memory System
3
+ * MCP Server for Postnesia Memory System
4
4
  * Exposes memory operations as Model Context Protocol tools
5
5
  */
6
- import 'dotenv/config';
6
+ export {};
package/dist/index.js CHANGED
@@ -1,514 +1,275 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MCP Server for Belle's Memory System
3
+ * MCP Server for Postnesia Memory System
4
4
  * Exposes memory operations as Model Context Protocol tools
5
5
  */
6
- import 'dotenv/config';
7
- import { Server } from "@modelcontextprotocol/sdk/server";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
+ import { z } from "zod";
10
9
  import { getDb, queries, createMemory } from "@postnesia/db";
11
10
  import { embed } from "@postnesia/db/embeddings";
12
- import { logAccess } from "./access.js";
13
- import { runConsolidation } from "./importance.js";
11
+ import { logAccess } from "@postnesia/db/access";
12
+ import { runConsolidation } from "@postnesia/db/importance";
14
13
  const db = getDb();
15
- const server = new Server({
16
- name: "openmind",
14
+ const server = new McpServer({
15
+ name: "postnesia",
17
16
  version: "1.0.0",
18
- }, {
19
- capabilities: {
20
- tools: {},
17
+ });
18
+ // ---------------------------------------------------------------------------
19
+ // memory tools
20
+ // ---------------------------------------------------------------------------
21
+ server.registerTool("memory_search", {
22
+ description: "Search memories by content. Returns all matching memories.",
23
+ inputSchema: {
24
+ query: z.string().describe("Search query"),
25
+ limit: z.number().optional().describe("Maximum results to return (default: 10)"),
21
26
  },
27
+ }, async ({ query, limit = 10 }) => {
28
+ const embedding = await embed(query);
29
+ const results = queries.vectorSearch(db).all(Buffer.from(embedding.buffer), limit);
30
+ for (const result of results) {
31
+ logAccess(result.id, "search");
32
+ }
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
35
+ };
22
36
  });
23
- // Tool definitions
24
- server.setRequestHandler(ListToolsRequestSchema, async () => {
37
+ server.registerTool("memory_add", {
38
+ description: "Add a new memory to the database",
39
+ inputSchema: {
40
+ content: z.string().describe("Memory content (full form)"),
41
+ contentL1: z.string().optional().describe("Compressed L1 form (optional, will auto-generate if omitted)"),
42
+ type: z.enum(["event", "decision", "lesson", "preference", "person", "technical"]).describe("Memory type"),
43
+ importance: z.number().describe("Base importance 1-5"),
44
+ tags: z.array(z.string()).describe("Tags for categorization"),
45
+ context: z.string().optional().describe("Optional context about when/why this memory was created"),
46
+ core: z.boolean().optional().describe("Mark as a core memory (always loaded, never decays, cannot be superseded)"),
47
+ },
48
+ }, async ({ content, contentL1, type, importance, tags, context, core }) => {
49
+ const content_l1 = contentL1 || content.slice(0, 200);
50
+ const embedding = await embed(content);
51
+ const id = createMemory(db, {
52
+ timestamp: new Date().toISOString(),
53
+ content,
54
+ content_l1,
55
+ type,
56
+ core: core ? 1 : 0,
57
+ importance,
58
+ context: context || undefined,
59
+ tags,
60
+ embedding,
61
+ });
25
62
  return {
26
- tools: [
27
- {
28
- name: "memory_search",
29
- description: "Search memories by content. Returns all matching memories.",
30
- inputSchema: {
31
- type: "object",
32
- properties: {
33
- query: {
34
- type: "string",
35
- description: "Search query",
36
- },
37
- limit: {
38
- type: "number",
39
- description: "Maximum results to return (default: 10)",
40
- },
41
- },
42
- required: ["query"],
43
- },
44
- },
45
- {
46
- name: "memory_add",
47
- description: "Add a new memory to the database",
48
- inputSchema: {
49
- type: "object",
50
- properties: {
51
- content: {
52
- type: "string",
53
- description: "Memory content (full form)",
54
- },
55
- contentL1: {
56
- type: "string",
57
- description: "Compressed L1 form (optional, will auto-generate if omitted)",
58
- },
59
- type: {
60
- type: "string",
61
- enum: ["event", "decision", "lesson", "preference", "person", "technical"],
62
- description: "Memory type",
63
- },
64
- importance: {
65
- type: "number",
66
- description: "Base importance 1-5",
67
- },
68
- tags: {
69
- type: "array",
70
- items: { type: "string" },
71
- description: "Tags for categorization",
72
- },
73
- context: {
74
- type: "string",
75
- description: "Optional context about when/why this memory was created",
76
- },
77
- core: {
78
- type: "boolean",
79
- description: "Mark as a core memory (always loaded, never decays, cannot be superseded)",
80
- },
81
- },
82
- required: ["content", "type", "importance", "tags"],
83
- },
84
- },
85
- {
86
- name: "memory_update_core",
87
- description: "Update the content of an existing core memory in place (core memories must be updated, never superseded)",
88
- inputSchema: {
89
- type: "object",
90
- properties: {
91
- memoryId: {
92
- type: "number",
93
- description: "ID of the memory to update",
94
- },
95
- content: {
96
- type: "string",
97
- description: "New full content",
98
- },
99
- contentL1: {
100
- type: "string",
101
- description: "New compressed L1 summary",
102
- },
103
- },
104
- required: ["memoryId", "content", "contentL1"],
105
- },
106
- },
107
- {
108
- name: "memory_recent",
109
- description: "Get recent memories within time window",
110
- inputSchema: {
111
- type: "object",
112
- properties: {
113
- hours: {
114
- type: "number",
115
- description: "Hours to look back (default: 24)",
116
- },
117
- limit: {
118
- type: "number",
119
- description: "Maximum results (default: 20)",
120
- },
121
- },
122
- },
123
- },
124
- {
125
- name: "memory_stats",
126
- description: "Get memory database statistics",
127
- inputSchema: {
128
- type: "object",
129
- properties: {},
130
- },
131
- },
132
- {
133
- name: "memory_consolidate",
134
- description: "Run memory consolidation cycle (decay old, boost accessed)",
135
- inputSchema: {
136
- type: "object",
137
- properties: {},
138
- },
139
- },
140
- {
141
- name: "journal_add",
142
- description: "Add a daily journal entry",
143
- inputSchema: {
144
- type: "object",
145
- properties: {
146
- date: {
147
- type: "string",
148
- description: "Date YYYY-MM-DD",
149
- },
150
- content: {
151
- type: "string",
152
- description: "Full journal narrative",
153
- },
154
- learned: {
155
- type: "string",
156
- description: "What I learned (optional)",
157
- },
158
- learnedAboutRye: {
159
- type: "string",
160
- description: "What I learned about Rye (optional)",
161
- },
162
- keyMoments: {
163
- type: "string",
164
- description: "Key moments (optional)",
165
- },
166
- mood: {
167
- type: "string",
168
- description: "Mood/feeling (optional)",
169
- },
170
- },
171
- required: ["date", "content"],
172
- },
173
- },
63
+ content: [
174
64
  {
175
- name: "journal_recent",
176
- description: "Get recent journal entries",
177
- inputSchema: {
178
- type: "object",
179
- properties: {
180
- days: {
181
- type: "number",
182
- description: "Days to look back (default: 7)",
183
- },
184
- },
185
- },
65
+ type: "text",
66
+ text: `Created memory #${id}\n Type: ${type}\n Core: ${core ? "yes" : "no"}\n Importance: ${importance}/5\n Tags: ${tags.join(", ")}`,
186
67
  },
68
+ ],
69
+ };
70
+ });
71
+ server.registerTool("memory_update_core", {
72
+ description: "Update the content of an existing core memory in place (core memories must be updated, never superseded)",
73
+ inputSchema: {
74
+ memoryId: z.number().describe("ID of the memory to update"),
75
+ content: z.string().describe("New full content"),
76
+ contentL1: z.string().describe("New compressed L1 summary"),
77
+ },
78
+ }, async ({ memoryId, content, contentL1 }) => {
79
+ const existing = db.prepare("SELECT id, core FROM memory WHERE id = ?").get(memoryId);
80
+ if (!existing)
81
+ throw new Error(`Memory #${memoryId} not found`);
82
+ if (!existing.core)
83
+ throw new Error(`Memory #${memoryId} is not a core memory. Use memory_add with supersedes_id for regular memories.`);
84
+ const embedding = await embed(content);
85
+ db.prepare(`
86
+ UPDATE memory
87
+ SET content = ?, content_l1 = ?, updated_at = datetime('now')
88
+ WHERE id = ?
89
+ `).run(content, contentL1, memoryId);
90
+ db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(BigInt(memoryId));
91
+ db.prepare("INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)").run(BigInt(memoryId), Buffer.from(embedding.buffer));
92
+ return {
93
+ content: [{ type: "text", text: `Updated core memory #${memoryId}` }],
94
+ };
95
+ });
96
+ server.registerTool("memory_recent", {
97
+ description: "Get recent memories within time window",
98
+ inputSchema: {
99
+ hours: z.number().optional().describe("Hours to look back (default: 24)"),
100
+ limit: z.number().optional().describe("Maximum results (default: 20)"),
101
+ },
102
+ }, async ({ hours = 24, limit = 20 }) => {
103
+ const results = queries.getRecentMemories(db).all(`-${hours} hours`, limit);
104
+ for (const result of results) {
105
+ logAccess(result.id, "recent");
106
+ }
107
+ return {
108
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
109
+ };
110
+ });
111
+ server.registerTool("memory_stats", {
112
+ description: "Get memory database statistics",
113
+ inputSchema: {},
114
+ }, async () => {
115
+ const stats = queries.getStats(db).all();
116
+ const total = db.prepare("SELECT COUNT(*) as count FROM memory").get().count;
117
+ const tags = db.prepare("SELECT COUNT(*) as count FROM tag").get().count;
118
+ const relationships = db.prepare("SELECT COUNT(*) as count FROM relationship").get().count;
119
+ const accessLogs = db.prepare("SELECT COUNT(*) as count FROM access_log").get().count;
120
+ const rows = stats
121
+ .map((s) => ` ${s.type.padEnd(15)} ${s.count.toString().padStart(3)} memories (avg importance: ${s.avg_importance?.toFixed(1)})`)
122
+ .join("\n");
123
+ return {
124
+ content: [
187
125
  {
188
- name: "memory_relationships",
189
- description: "View relationship graph for a memory",
190
- inputSchema: {
191
- type: "object",
192
- properties: {
193
- memoryId: {
194
- type: "number",
195
- description: "Memory ID to explore",
196
- },
197
- },
198
- required: ["memoryId"],
199
- },
126
+ type: "text",
127
+ text: `Memory Statistics:\n\n${rows}\n\n Total: ${total} memories\n Tags: ${tags}\n Relationships: ${relationships}\n Access Logs: ${accessLogs}`,
200
128
  },
129
+ ],
130
+ };
131
+ });
132
+ server.registerTool("memory_consolidate", {
133
+ description: "Run memory consolidation cycle (decay old, boost accessed)",
134
+ inputSchema: {},
135
+ }, async () => {
136
+ const results = runConsolidation();
137
+ return {
138
+ content: [
201
139
  {
202
- name: "task_create",
203
- description: "Create a new task. Use session_id to group tasks by project or feature.",
204
- inputSchema: {
205
- type: "object",
206
- properties: {
207
- title: {
208
- type: "string",
209
- description: "Short task title",
210
- },
211
- description: {
212
- type: "string",
213
- description: "Detailed description of what needs to be done",
214
- },
215
- session_id: {
216
- type: "string",
217
- description: "Project or feature label to group related tasks (e.g. 'openmind-mcp', 'auth-refactor')",
218
- },
219
- memory_id: {
220
- type: "number",
221
- description: "Optional ID of a related memory",
222
- },
223
- },
224
- required: ["title"],
225
- },
140
+ type: "text",
141
+ text: `Consolidation Complete\n\nReviewed: ${results.reviewed} memories\nBoosted: ${results.boosted} memories\nDecayed: ${results.decayed} memories\nUnchanged: ${results.unchanged} memories\n\nChanges saved to database`,
226
142
  },
143
+ ],
144
+ };
145
+ });
146
+ server.registerTool("memory_relationships", {
147
+ description: "View relationship graph for a memory",
148
+ inputSchema: {
149
+ memoryId: z.number().describe("Memory ID to explore"),
150
+ },
151
+ }, async ({ memoryId }) => {
152
+ const relationships = queries.getMemoryRelationships(db).all(memoryId, memoryId);
153
+ return {
154
+ content: [{ type: "text", text: JSON.stringify(relationships, null, 2) }],
155
+ };
156
+ });
157
+ // ---------------------------------------------------------------------------
158
+ // journal tools
159
+ // ---------------------------------------------------------------------------
160
+ server.registerTool("journal_add", {
161
+ description: "Add a daily journal entry",
162
+ inputSchema: {
163
+ date: z.string().describe("Date YYYY-MM-DD"),
164
+ content: z.string().describe("Full journal narrative"),
165
+ learned: z.string().optional().describe("What I learned (optional)"),
166
+ learnedAboutRye: z.string().optional().describe("What I learned about Rye (optional)"),
167
+ keyMoments: z.string().optional().describe("Key moments (optional)"),
168
+ mood: z.string().optional().describe("Mood/feeling (optional)"),
169
+ },
170
+ }, async ({ date, content, learned, learnedAboutRye, keyMoments, mood }) => {
171
+ const combinedLearned = [learned, learnedAboutRye].filter(Boolean).join("\n\n") || null;
172
+ queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
173
+ return {
174
+ content: [{ type: "text", text: `Journal entry created for ${date}` }],
175
+ };
176
+ });
177
+ server.registerTool("journal_recent", {
178
+ description: "Get recent journal entries",
179
+ inputSchema: {
180
+ days: z.number().optional().describe("Days to look back (default: 7)"),
181
+ },
182
+ }, async ({ days = 7 }) => {
183
+ const entries = queries.getRecentJournals(db).all(`-${days} days`);
184
+ return {
185
+ content: [{ type: "text", text: JSON.stringify(entries, null, 2) }],
186
+ };
187
+ });
188
+ // ---------------------------------------------------------------------------
189
+ // task tools
190
+ // ---------------------------------------------------------------------------
191
+ server.registerTool("task_create", {
192
+ description: "Create a new task. Use session_id to group tasks by project or feature.",
193
+ inputSchema: {
194
+ title: z.string().describe("Short task title"),
195
+ description: z.string().optional().describe("Detailed description of what needs to be done"),
196
+ session_id: z.string().optional().describe("Project or feature label to group related tasks (e.g. 'openmind-mcp', 'auth-refactor')"),
197
+ memory_id: z.number().optional().describe("Optional ID of a related memory"),
198
+ },
199
+ }, async ({ title, description, session_id, memory_id }) => {
200
+ const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
201
+ const id = Number(result.lastInsertRowid);
202
+ return {
203
+ content: [
227
204
  {
228
- name: "task_update",
229
- description: "Update a task's status, title, or description",
230
- inputSchema: {
231
- type: "object",
232
- properties: {
233
- taskId: {
234
- type: "number",
235
- description: "ID of the task to update",
236
- },
237
- status: {
238
- type: "string",
239
- enum: ["pending", "in_progress", "completed", "cancelled"],
240
- description: "New status",
241
- },
242
- title: {
243
- type: "string",
244
- description: "Updated title",
245
- },
246
- description: {
247
- type: "string",
248
- description: "Updated description",
249
- },
250
- },
251
- required: ["taskId"],
252
- },
205
+ type: "text",
206
+ text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ""}`,
253
207
  },
208
+ ],
209
+ };
210
+ });
211
+ server.registerTool("task_update", {
212
+ description: "Update a task's status, title, or description",
213
+ inputSchema: {
214
+ taskId: z.number().describe("ID of the task to update"),
215
+ status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe("New status"),
216
+ title: z.string().optional().describe("Updated title"),
217
+ description: z.string().optional().describe("Updated description"),
218
+ },
219
+ }, async ({ taskId, status, title, description }) => {
220
+ const existing = queries.getTaskById(db).get(taskId);
221
+ if (!existing)
222
+ throw new Error(`Task #${taskId} not found`);
223
+ queries.updateTask(db).run(status || null, title || null, description || null, taskId);
224
+ const updated = queries.getTaskById(db).get(taskId);
225
+ return {
226
+ content: [
254
227
  {
255
- name: "task_list",
256
- description: "List tasks, optionally filtered by status and/or session_id. Use at session start to resume open work.",
257
- inputSchema: {
258
- type: "object",
259
- properties: {
260
- status: {
261
- type: "string",
262
- enum: ["pending", "in_progress", "completed", "cancelled"],
263
- description: "Filter by status",
264
- },
265
- session_id: {
266
- type: "string",
267
- description: "Filter by project/feature label",
268
- },
269
- limit: {
270
- type: "number",
271
- description: "Maximum results (default: 50)",
272
- },
273
- },
274
- },
228
+ type: "text",
229
+ text: `Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
275
230
  },
276
231
  ],
277
232
  };
278
233
  });
279
- // Tool handlers
280
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
281
- try {
282
- const { name, arguments: args = {} } = request.params;
283
- switch (name) {
284
- case "memory_search": {
285
- const query = args.query.toLowerCase();
286
- const limit = args.limit || 10;
287
- const pattern = `%${query}%`;
288
- const results = queries.searchMemories(db).all(pattern, pattern, limit);
289
- for (const result of results) {
290
- logAccess(result.id, 'search');
291
- }
292
- return {
293
- content: [
294
- {
295
- type: "text",
296
- text: JSON.stringify(results, null, 2),
297
- },
298
- ],
299
- };
300
- }
301
- case "memory_add": {
302
- const { content, contentL1, type, importance, tags, context, core } = args;
303
- const content_l1 = contentL1 || content.slice(0, 200);
304
- const embedding = await embed(content);
305
- const id = createMemory(db, {
306
- timestamp: new Date().toISOString(),
307
- content,
308
- content_l1,
309
- type,
310
- core: core ? 1 : 0,
311
- importance,
312
- context: context || undefined,
313
- tags,
314
- embedding,
315
- });
316
- return {
317
- content: [
318
- {
319
- type: "text",
320
- text: `✓ Created memory #${id}\n Type: ${type}\n Core: ${core ? 'yes' : 'no'}\n Importance: ${'⭐'.repeat(importance)}\n Tags: ${tags.join(', ')}`,
321
- },
322
- ],
323
- };
324
- }
325
- case "memory_update_core": {
326
- const { memoryId, content, contentL1 } = args;
327
- const existing = db.prepare('SELECT id, core FROM memory WHERE id = ?').get(memoryId);
328
- if (!existing) {
329
- throw new Error(`Memory #${memoryId} not found`);
330
- }
331
- if (!existing.core) {
332
- throw new Error(`Memory #${memoryId} is not a core memory. Use memory_add with supersedes_id for regular memories.`);
333
- }
334
- const embedding = await embed(content);
335
- db.prepare(`
336
- UPDATE memory
337
- SET content = ?, content_l1 = ?, updated_at = datetime('now')
338
- WHERE id = ?
339
- `).run(content, contentL1, memoryId);
340
- // Update embedding in vec_memories virtual table
341
- db.prepare('DELETE FROM vec_memories WHERE memory_id = ?').run(BigInt(memoryId));
342
- db.prepare('INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)').run(BigInt(memoryId), Buffer.from(embedding.buffer));
343
- return {
344
- content: [
345
- {
346
- type: "text",
347
- text: `✓ Updated core memory #${memoryId}`,
348
- },
349
- ],
350
- };
351
- }
352
- case "memory_recent": {
353
- const hours = args.hours || 24;
354
- const limit = args.limit || 20;
355
- const results = queries.getRecentMemories(db).all(`-${hours} hours`, limit);
356
- for (const result of results) {
357
- logAccess(result.id, 'recent');
358
- }
359
- return {
360
- content: [
361
- {
362
- type: "text",
363
- text: JSON.stringify(results, null, 2),
364
- },
365
- ],
366
- };
367
- }
368
- case "memory_stats": {
369
- const stats = queries.getStats(db).all();
370
- const total = db.prepare('SELECT COUNT(*) as count FROM memory').get().count;
371
- const tags = db.prepare('SELECT COUNT(*) as count FROM tag').get().count;
372
- const relationships = db.prepare('SELECT COUNT(*) as count FROM relationship').get().count;
373
- const accessLogs = db.prepare('SELECT COUNT(*) as count FROM access_log').get().count;
374
- return {
375
- content: [
376
- {
377
- type: "text",
378
- text: `📊 Memory Statistics:\n\n${stats.map(s => ` ${s.type.padEnd(15)} ${s.count.toString().padStart(3)} memories (avg importance: ${s.avg_importance?.toFixed(1)})`).join('\n')}\n\n Total: ${total} memories\n Tags: ${tags}\n Relationships: ${relationships}\n Access Logs: ${accessLogs}`,
379
- },
380
- ],
381
- };
382
- }
383
- case "memory_consolidate": {
384
- const results = runConsolidation();
385
- return {
386
- content: [
387
- {
388
- type: "text",
389
- text: `🔄 Consolidation Complete\n\nReviewed: ${results.reviewed} memories\nBoosted: ${results.boosted} memories\nDecayed: ${results.decayed} memories\nUnchanged: ${results.unchanged} memories\n\nChanges saved to database`,
390
- },
391
- ],
392
- };
393
- }
394
- case "journal_add": {
395
- const { date, content, learned, learnedAboutRye, keyMoments, mood } = args;
396
- // Combine learned + learnedAboutRye into the single learned column
397
- const combinedLearned = [learned, learnedAboutRye].filter(Boolean).join('\n\n') || null;
398
- queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
399
- return {
400
- content: [
401
- {
402
- type: "text",
403
- text: `✓ Journal entry created for ${date}`,
404
- },
405
- ],
406
- };
407
- }
408
- case "journal_recent": {
409
- const days = args.days || 7;
410
- const entries = queries.getRecentJournals(db).all(`-${days} days`);
411
- return {
412
- content: [
413
- {
414
- type: "text",
415
- text: JSON.stringify(entries, null, 2),
416
- },
417
- ],
418
- };
419
- }
420
- case "memory_relationships": {
421
- const memoryId = args.memoryId;
422
- const relationships = queries.getMemoryRelationships(db).all(memoryId, memoryId);
423
- return {
424
- content: [
425
- {
426
- type: "text",
427
- text: JSON.stringify(relationships, null, 2),
428
- },
429
- ],
430
- };
431
- }
432
- case "task_create": {
433
- const { title, description, session_id, memory_id } = args;
434
- const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
435
- const id = Number(result.lastInsertRowid);
436
- return {
437
- content: [
438
- {
439
- type: "text",
440
- text: `✓ Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ''}`,
441
- },
442
- ],
443
- };
444
- }
445
- case "task_update": {
446
- const { taskId, status, title, description } = args;
447
- const existing = queries.getTaskById(db).get(taskId);
448
- if (!existing)
449
- throw new Error(`Task #${taskId} not found`);
450
- queries.updateTask(db).run(status || null, title || null, description || null, taskId);
451
- const updated = queries.getTaskById(db).get(taskId);
452
- return {
453
- content: [
454
- {
455
- type: "text",
456
- text: `✓ Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
457
- },
458
- ],
459
- };
460
- }
461
- case "task_list": {
462
- const { status, session_id, limit = 50 } = args;
463
- // Build dynamic WHERE clause
464
- const conditions = [];
465
- const params = [];
466
- if (status) {
467
- conditions.push('status = ?');
468
- params.push(status);
469
- }
470
- if (session_id) {
471
- conditions.push('session_id = ?');
472
- params.push(session_id);
473
- }
474
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
475
- params.push(limit);
476
- const tasks = db.prepare(`
477
- SELECT * FROM task
478
- ${where}
479
- ORDER BY created_at ASC
480
- LIMIT ?
481
- `).all(...params);
482
- return {
483
- content: [
484
- {
485
- type: "text",
486
- text: JSON.stringify(tasks, null, 2),
487
- },
488
- ],
489
- };
490
- }
491
- default:
492
- throw new Error(`Unknown tool: ${name}`);
493
- }
234
+ server.registerTool("task_list", {
235
+ description: "List tasks, optionally filtered by status and/or session_id. Use at session start to resume open work.",
236
+ inputSchema: {
237
+ status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe("Filter by status"),
238
+ session_id: z.string().optional().describe("Filter by project/feature label"),
239
+ limit: z.number().optional().describe("Maximum results (default: 50)"),
240
+ },
241
+ }, async ({ status, session_id, limit = 50 }) => {
242
+ const conditions = [];
243
+ const params = [];
244
+ if (status) {
245
+ conditions.push("status = ?");
246
+ params.push(status);
494
247
  }
495
- catch (error) {
496
- return {
497
- content: [
498
- {
499
- type: "text",
500
- text: `Error: ${error.message}`,
501
- },
502
- ],
503
- isError: true,
504
- };
248
+ if (session_id) {
249
+ conditions.push("session_id = ?");
250
+ params.push(session_id);
505
251
  }
252
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
253
+ params.push(limit);
254
+ const tasks = db.prepare(`
255
+ SELECT * FROM task
256
+ ${where}
257
+ ORDER BY created_at ASC
258
+ LIMIT ?
259
+ `).all(...params);
260
+ return {
261
+ content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
262
+ };
506
263
  });
264
+ // ---------------------------------------------------------------------------
507
265
  // Start server
508
- try {
266
+ // ---------------------------------------------------------------------------
267
+ async function main() {
509
268
  const transport = new StdioServerTransport();
510
269
  await server.connect(transport);
270
+ console.error("Postnesia MCP Server running on stdio");
511
271
  }
512
- catch (error) {
513
- console.error("OpenMind Memory MCP Server running on stdio");
514
- }
272
+ main().catch((error) => {
273
+ console.error("Fatal error:", error);
274
+ process.exit(1);
275
+ });
package/package.json CHANGED
@@ -1,27 +1,19 @@
1
1
  {
2
2
  "name": "@postnesia/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "An MCP server to interact with the Postnesia database",
5
5
  "type": "module",
6
+ "private": false,
6
7
  "files": [
7
8
  "dist"
8
9
  ],
9
- "exports": {
10
- "./access": {
11
- "types": "./dist/access.d.ts",
12
- "import": "./dist/access.js"
13
- },
14
- "./importance": {
15
- "types": "./dist/importance.d.ts",
16
- "import": "./dist/importance.js"
17
- }
18
- },
19
10
  "bin": {
20
11
  "postnesia-mcp": "./dist/index.js"
21
12
  },
22
13
  "dependencies": {
23
14
  "@modelcontextprotocol/sdk": "^1.26.0",
24
- "@postnesia/db": "0.1.4"
15
+ "@postnesia/db": "^0.1.5",
16
+ "zod": "^4.3.6"
25
17
  },
26
18
  "devDependencies": {
27
19
  "@types/node": "^22.10.5",
@@ -29,7 +21,8 @@
29
21
  "typescript": "^5.7.3"
30
22
  },
31
23
  "scripts": {
32
- "build": "tsc -p tsconfig.json",
24
+ "build": "pnpm clean && tsc -p tsconfig.json",
25
+ "clean": "rm -rf ./dist",
33
26
  "start": "tsx src/index.ts"
34
27
  }
35
28
  }
package/dist/access.d.ts DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * Access Tracking System
3
- * Logs when memories are retrieved and updates last_accessed on the memory row.
4
- * Feeds into L1 decay calculations.
5
- */
6
- export interface AccessLogEntry {
7
- memory_id: number;
8
- context?: string;
9
- accessed_at: string;
10
- }
11
- /**
12
- * Record that a memory was accessed (updates both access_log and memory.last_accessed)
13
- */
14
- export declare function logAccess(memory_id: number, context?: string): void;
15
- export declare function getAccessCount(memory_id: number, daysBack?: number): number;
16
- export declare function getRecentlyAccessed(limit?: number): number[];
17
- export declare function getAccessHistory(memory_id: number): AccessLogEntry[];
18
- /**
19
- * Calculate relevance boost based on access patterns
20
- * Recent access + frequency = higher relevance
21
- */
22
- export declare function calculateAccessBoost(memory_id: number): number;
package/dist/access.js DELETED
@@ -1,63 +0,0 @@
1
- /**
2
- * Access Tracking System
3
- * Logs when memories are retrieved and updates last_accessed on the memory row.
4
- * Feeds into L1 decay calculations.
5
- */
6
- import { getDb, recordAccess } from '@postnesia/db';
7
- /**
8
- * Record that a memory was accessed (updates both access_log and memory.last_accessed)
9
- */
10
- export function logAccess(memory_id, context) {
11
- try {
12
- const db = getDb(false);
13
- recordAccess(db, memory_id, context);
14
- }
15
- catch (error) {
16
- console.error(`[access-tracker] Warning: Could not log access for #${memory_id}:`, error?.message);
17
- }
18
- }
19
- export function getAccessCount(memory_id, daysBack = 30) {
20
- const db = getDb(true);
21
- const result = db.prepare(`
22
- SELECT COUNT(*) as count
23
- FROM access_log
24
- WHERE memory_id = ?
25
- AND accessed_at > datetime('now', '-${daysBack} days')
26
- `).get(memory_id);
27
- return result.count;
28
- }
29
- export function getRecentlyAccessed(limit = 20) {
30
- const db = getDb(true);
31
- const results = db.prepare(`
32
- SELECT memory_id, COUNT(*) as access_count
33
- FROM access_log
34
- WHERE accessed_at > datetime('now', '-7 days')
35
- GROUP BY memory_id
36
- ORDER BY access_count DESC, MAX(accessed_at) DESC
37
- LIMIT ?
38
- `).all(limit);
39
- return results.map(r => r.memory_id);
40
- }
41
- export function getAccessHistory(memory_id) {
42
- const db = getDb(true);
43
- return db.prepare(`
44
- SELECT memory_id, accessed_at, context
45
- FROM access_log
46
- WHERE memory_id = ?
47
- ORDER BY accessed_at DESC
48
- LIMIT 50
49
- `).all(memory_id);
50
- }
51
- /**
52
- * Calculate relevance boost based on access patterns
53
- * Recent access + frequency = higher relevance
54
- */
55
- export function calculateAccessBoost(memory_id) {
56
- const last7Days = getAccessCount(memory_id, 7);
57
- const last30Days = getAccessCount(memory_id, 30);
58
- const recentWeight = last7Days * 2;
59
- const olderWeight = (last30Days - last7Days);
60
- const totalScore = recentWeight + olderWeight;
61
- // Normalize to 0-2 range
62
- return Math.min(2, totalScore / 5);
63
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Importance Dynamics
3
- * Adjust memory importance based on age, access patterns, and relationships
4
- */
5
- export interface ImportanceFactors {
6
- baseImportance: number;
7
- ageDecay: number;
8
- accessBoost: number;
9
- relationshipBoost: number;
10
- finalScore: number;
11
- }
12
- /**
13
- * Calculate age decay factor
14
- * Recent memories maintain importance, old ones decay unless accessed
15
- */
16
- export declare function calculateAgeDecay(timestamp: string): number;
17
- /**
18
- * Calculate dynamic importance score
19
- * Base importance + access boost - age decay
20
- */
21
- export declare function calculateDynamicImportance(memory_id: number): ImportanceFactors;
22
- /**
23
- * Get memories eligible for L1 based on dynamic scoring.
24
- * Uses the decay query from PHILOSOPHY.md: importance penalized by last_accessed staleness,
25
- * superseded memories excluded.
26
- */
27
- export declare function getL1Candidates(limit?: number): Array<{
28
- id: number;
29
- type: string;
30
- content_l1: string;
31
- timestamp: string;
32
- last_accessed: string;
33
- baseImportance: number;
34
- dynamicScore: number;
35
- }>;
36
- /**
37
- * Consolidation: Review and adjust importances based on access patterns
38
- */
39
- export declare function runConsolidation(): {
40
- reviewed: number;
41
- boosted: number;
42
- decayed: number;
43
- unchanged: number;
44
- };
@@ -1,145 +0,0 @@
1
- /**
2
- * Importance Dynamics
3
- * Adjust memory importance based on age, access patterns, and relationships
4
- */
5
- import { getDb, queries } from '@postnesia/db';
6
- import { calculateAccessBoost } from './access.js';
7
- /**
8
- * Calculate relationship boost based on graph connectivity
9
- * Memories that are hubs in the relationship graph are more valuable
10
- */
11
- function calculateRelationshipBoost(memory_id) {
12
- const db = getDb(true);
13
- // Count total connections
14
- const connections = db.prepare(`
15
- SELECT COUNT(*) as count
16
- FROM relationship
17
- WHERE from_id = ? OR to_id = ?
18
- `).get(memory_id, memory_id);
19
- // Count connections to high-importance memories (4+)
20
- const importantConnections = db.prepare(`
21
- SELECT COUNT(*) as count
22
- FROM relationship r
23
- JOIN memory m ON (
24
- CASE
25
- WHEN r.from_id = ? THEN m.id = r.to_id
26
- ELSE m.id = r.from_id
27
- END
28
- )
29
- WHERE (r.from_id = ? OR r.to_id = ?)
30
- AND m.importance >= 4
31
- `).get(memory_id, memory_id, memory_id);
32
- // Boost calculation:
33
- // - Base: 0.1 per connection (up to +1.0 for 10 connections)
34
- // - Bonus: +0.2 per important connection
35
- const baseBoost = Math.min(1.0, connections.count * 0.1);
36
- const importantBonus = importantConnections.count * 0.2;
37
- return Math.min(2.0, baseBoost + importantBonus);
38
- }
39
- /**
40
- * Calculate age decay factor
41
- * Recent memories maintain importance, old ones decay unless accessed
42
- */
43
- export function calculateAgeDecay(timestamp) {
44
- const now = Date.now();
45
- const memoryDate = new Date(timestamp).getTime();
46
- const ageInDays = (now - memoryDate) / (1000 * 60 * 60 * 24);
47
- // No decay for first 7 days
48
- if (ageInDays <= 7)
49
- return 0;
50
- // Gradual decay: -0.1 per week after first week
51
- const weeksOld = Math.floor((ageInDays - 7) / 7);
52
- const decay = Math.min(2, weeksOld * 0.1); // Max -2 importance
53
- return -decay;
54
- }
55
- /**
56
- * Calculate dynamic importance score
57
- * Base importance + access boost - age decay
58
- */
59
- export function calculateDynamicImportance(memory_id) {
60
- const db = getDb(true);
61
- const memory = db.prepare(`
62
- SELECT importance, timestamp
63
- FROM memory
64
- WHERE id = ?
65
- `).get(memory_id);
66
- if (!memory) {
67
- return {
68
- baseImportance: 0,
69
- ageDecay: 0,
70
- accessBoost: 0,
71
- relationshipBoost: 0,
72
- finalScore: 0,
73
- };
74
- }
75
- const baseImportance = memory.importance;
76
- const ageDecay = calculateAgeDecay(memory.timestamp);
77
- const accessBoost = calculateAccessBoost(memory_id);
78
- const relationshipBoost = calculateRelationshipBoost(memory_id);
79
- // Final score: base + boosts + decay (decay is negative)
80
- const finalScore = Math.max(1, Math.min(5, baseImportance + accessBoost + relationshipBoost + ageDecay));
81
- return {
82
- baseImportance,
83
- ageDecay,
84
- accessBoost,
85
- relationshipBoost,
86
- finalScore: Math.round(finalScore * 10) / 10, // Round to 1 decimal
87
- };
88
- }
89
- /**
90
- * Get memories eligible for L1 based on dynamic scoring.
91
- * Uses the decay query from PHILOSOPHY.md: importance penalized by last_accessed staleness,
92
- * superseded memories excluded.
93
- */
94
- export function getL1Candidates(limit = 50) {
95
- const db = getDb(true);
96
- // Use the L1 decay query from db.ts (matches PHILOSOPHY.md)
97
- const memories = queries.getL1Summaries(db).all();
98
- return memories.slice(0, limit).map(m => ({
99
- id: m.id,
100
- type: m.type,
101
- content_l1: m.content_l1,
102
- timestamp: m.timestamp,
103
- last_accessed: m.last_accessed,
104
- baseImportance: m.importance,
105
- dynamicScore: m.effective_importance,
106
- }));
107
- }
108
- /**
109
- * Consolidation: Review and adjust importances based on access patterns
110
- */
111
- export function runConsolidation() {
112
- const db = getDb(false);
113
- let reviewed = 0;
114
- let boosted = 0;
115
- let decayed = 0;
116
- let unchanged = 0;
117
- // Get all memories from last 60 days
118
- const memories = db.prepare(`
119
- SELECT id, importance, timestamp
120
- FROM memory
121
- WHERE timestamp > datetime('now', '-60 days')
122
- `).all();
123
- for (const memory of memories) {
124
- reviewed++;
125
- const factors = calculateDynamicImportance(memory.id);
126
- const newImportance = Math.round(factors.finalScore);
127
- if (newImportance !== memory.importance) {
128
- db.prepare(`
129
- UPDATE memory
130
- SET importance = ?
131
- WHERE id = ?
132
- `).run(newImportance, memory.id);
133
- if (newImportance > memory.importance) {
134
- boosted++;
135
- }
136
- else {
137
- decayed++;
138
- }
139
- }
140
- else {
141
- unchanged++;
142
- }
143
- }
144
- return { reviewed, boosted, decayed, unchanged };
145
- }