@postnesia/mcp 0.1.14 → 0.1.16
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/dist/index.js +170 -137
- package/dist/install.js +6 -6
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -3,28 +3,28 @@
|
|
|
3
3
|
* MCP Server for Postnesia Memory System
|
|
4
4
|
* Exposes memory operations as Model Context Protocol tools
|
|
5
5
|
*/
|
|
6
|
-
import { McpServer } from
|
|
7
|
-
import { StdioServerTransport } from
|
|
8
|
-
import { z } from
|
|
9
|
-
import { getDb, closeDb, createMemory } from
|
|
10
|
-
import queries from
|
|
11
|
-
import { embed } from
|
|
12
|
-
import { logAccess } from
|
|
13
|
-
import { runConsolidation } from
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { getDb, closeDb, createMemory } from '@postnesia/db';
|
|
10
|
+
import queries from '@postnesia/db/queries';
|
|
11
|
+
import { embed } from '@postnesia/db/embeddings';
|
|
12
|
+
import { logAccess } from '@postnesia/db/access';
|
|
13
|
+
import { runConsolidation } from '@postnesia/db/importance';
|
|
14
14
|
const db = getDb();
|
|
15
15
|
const server = new McpServer({
|
|
16
|
-
name:
|
|
17
|
-
version:
|
|
16
|
+
name: 'postnesia',
|
|
17
|
+
version: '1.0.0',
|
|
18
18
|
});
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// memory tools
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
|
-
server.registerTool(
|
|
23
|
-
description:
|
|
22
|
+
server.registerTool('memory_search', {
|
|
23
|
+
description: 'Search memories by semantic similarity. Optionally filter by type.',
|
|
24
24
|
inputSchema: {
|
|
25
|
-
query: z.string().describe(
|
|
26
|
-
limit: z.number().optional().describe(
|
|
27
|
-
type: z.enum([
|
|
25
|
+
query: z.string().describe('Search query'),
|
|
26
|
+
limit: z.number().optional().describe('Maximum results to return (default: 10)'),
|
|
27
|
+
type: z.enum(['event', 'decision', 'lesson', 'preference', 'person', 'technical']).optional().describe('Filter by memory type'),
|
|
28
28
|
},
|
|
29
29
|
}, async ({ query, limit = 10, type }) => {
|
|
30
30
|
const [embedding] = await embed(query);
|
|
@@ -36,86 +36,86 @@ server.registerTool("memory_search", {
|
|
|
36
36
|
? queries.vectorSearchByType(db).all(embeddingBuffer, limit, type)
|
|
37
37
|
: queries.vectorSearch(db).all(embeddingBuffer, limit);
|
|
38
38
|
for (const result of results) {
|
|
39
|
-
logAccess(result.id,
|
|
39
|
+
logAccess(result.id, 'search');
|
|
40
40
|
}
|
|
41
41
|
return {
|
|
42
|
-
content: [{ type:
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
43
43
|
};
|
|
44
44
|
});
|
|
45
|
-
server.registerTool(
|
|
46
|
-
description:
|
|
45
|
+
server.registerTool('memory_search_keyword', {
|
|
46
|
+
description: 'Keyword search memories by content (use when semantic search is not appropriate).',
|
|
47
47
|
inputSchema: {
|
|
48
|
-
query: z.string().describe(
|
|
49
|
-
limit: z.number().optional().describe(
|
|
48
|
+
query: z.string().describe('Keyword or phrase to search for'),
|
|
49
|
+
limit: z.number().optional().describe('Maximum results to return (default: 10)'),
|
|
50
50
|
},
|
|
51
51
|
}, async ({ query, limit = 10 }) => {
|
|
52
52
|
const pattern = `%${query}%`;
|
|
53
53
|
const results = queries.searchMemories(db).all(pattern, pattern, limit);
|
|
54
54
|
for (const result of results) {
|
|
55
|
-
logAccess(result.id,
|
|
55
|
+
logAccess(result.id, 'search');
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
|
-
content: [{ type:
|
|
58
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
59
59
|
};
|
|
60
60
|
});
|
|
61
|
-
server.registerTool(
|
|
62
|
-
description:
|
|
61
|
+
server.registerTool('memory_by_context', {
|
|
62
|
+
description: 'Find memories by context string (LIKE match).',
|
|
63
63
|
inputSchema: {
|
|
64
|
-
context: z.string().describe(
|
|
65
|
-
limit: z.number().optional().describe(
|
|
64
|
+
context: z.string().describe('Context string to search for'),
|
|
65
|
+
limit: z.number().optional().describe('Maximum results (default: 10)'),
|
|
66
66
|
},
|
|
67
67
|
}, async ({ context, limit = 10 }) => {
|
|
68
68
|
const results = queries.getMemoriesByContext(db).all(`%${context}%`, limit);
|
|
69
69
|
for (const r of results)
|
|
70
|
-
logAccess(r.id,
|
|
70
|
+
logAccess(r.id, 'search');
|
|
71
71
|
return {
|
|
72
|
-
content: [{ type:
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
|
-
server.registerTool(
|
|
76
|
-
description:
|
|
75
|
+
server.registerTool('memory_by_tag', {
|
|
76
|
+
description: 'Find memories by tag (exact match).',
|
|
77
77
|
inputSchema: {
|
|
78
|
-
tag: z.string().describe(
|
|
79
|
-
limit: z.number().optional().describe(
|
|
78
|
+
tag: z.string().describe('Tag to filter by'),
|
|
79
|
+
limit: z.number().optional().describe('Maximum results (default: 10)'),
|
|
80
80
|
},
|
|
81
81
|
}, async ({ tag, limit = 10 }) => {
|
|
82
82
|
const results = queries.getMemoriesByTag(db).all(tag, limit);
|
|
83
83
|
for (const r of results)
|
|
84
|
-
logAccess(r.id,
|
|
84
|
+
logAccess(r.id, 'search');
|
|
85
85
|
return {
|
|
86
|
-
content: [{ type:
|
|
86
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
87
87
|
};
|
|
88
88
|
});
|
|
89
|
-
server.registerTool(
|
|
90
|
-
description:
|
|
89
|
+
server.registerTool('memory_supersede_chain', {
|
|
90
|
+
description: 'Trace the supersede chain for a memory (shows the full replacement history).',
|
|
91
91
|
inputSchema: {
|
|
92
|
-
memoryId: z.number().describe(
|
|
92
|
+
memoryId: z.number().describe('Memory ID to trace'),
|
|
93
93
|
},
|
|
94
94
|
}, async ({ memoryId }) => {
|
|
95
95
|
const chain = queries.getSupersedeChain(db).all(memoryId);
|
|
96
96
|
return {
|
|
97
|
-
content: [{ type:
|
|
97
|
+
content: [{ type: 'text', text: JSON.stringify(chain, null, 2) }],
|
|
98
98
|
};
|
|
99
99
|
});
|
|
100
|
-
server.registerTool(
|
|
101
|
-
description:
|
|
100
|
+
server.registerTool('memory_core', {
|
|
101
|
+
description: 'Get all core memories (foundational memories that are always loaded).',
|
|
102
102
|
inputSchema: {},
|
|
103
103
|
}, async () => {
|
|
104
104
|
const results = queries.coreMemories(db).all();
|
|
105
105
|
return {
|
|
106
|
-
content: [{ type:
|
|
106
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
107
107
|
};
|
|
108
108
|
});
|
|
109
|
-
server.registerTool(
|
|
110
|
-
description:
|
|
109
|
+
server.registerTool('memory_add', {
|
|
110
|
+
description: 'Add a new memory to the database',
|
|
111
111
|
inputSchema: {
|
|
112
|
-
content: z.string().describe(
|
|
113
|
-
contentL1: z.string().optional().describe(
|
|
114
|
-
type: z.enum([
|
|
115
|
-
importance: z.number().describe(
|
|
116
|
-
tags: z.array(z.string()).describe(
|
|
117
|
-
context: z.string().optional().describe(
|
|
118
|
-
core: z.boolean().optional().describe(
|
|
112
|
+
content: z.string().describe('Memory content (full form)'),
|
|
113
|
+
contentL1: z.string().optional().describe('Compressed L1 form (optional, will auto-generate if omitted)'),
|
|
114
|
+
type: z.enum(['event', 'decision', 'lesson', 'preference', 'person', 'technical']).describe('Memory type'),
|
|
115
|
+
importance: z.number().describe('Base importance 1-5'),
|
|
116
|
+
tags: z.array(z.string()).describe('Tags for categorization'),
|
|
117
|
+
context: z.string().optional().describe('Optional context about when/why this memory was created'),
|
|
118
|
+
core: z.boolean().optional().describe('Mark as a core memory (always loaded, never decays, cannot be superseded)'),
|
|
119
119
|
},
|
|
120
120
|
}, async ({ content, contentL1, type, importance, tags, context, core }) => {
|
|
121
121
|
const content_l1 = contentL1 || content.slice(0, 200);
|
|
@@ -137,21 +137,21 @@ server.registerTool("memory_add", {
|
|
|
137
137
|
return {
|
|
138
138
|
content: [
|
|
139
139
|
{
|
|
140
|
-
type:
|
|
141
|
-
text: `Created memory #${id}\n Type: ${type}\n Core: ${core ?
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: `Created memory #${id}\n Type: ${type}\n Core: ${core ? 'yes' : 'no'}\n Importance: ${importance}/5\n Tags: ${tags.join(', ')}`,
|
|
142
142
|
},
|
|
143
143
|
],
|
|
144
144
|
};
|
|
145
145
|
});
|
|
146
|
-
server.registerTool(
|
|
147
|
-
description:
|
|
146
|
+
server.registerTool('memory_update_core', {
|
|
147
|
+
description: 'Update the content of an existing core memory in place (core memories must be updated, never superseded)',
|
|
148
148
|
inputSchema: {
|
|
149
|
-
memoryId: z.number().describe(
|
|
150
|
-
content: z.string().describe(
|
|
151
|
-
contentL1: z.string().describe(
|
|
149
|
+
memoryId: z.number().describe('ID of the memory to update'),
|
|
150
|
+
content: z.string().describe('New full content'),
|
|
151
|
+
contentL1: z.string().describe('New compressed L1 summary'),
|
|
152
152
|
},
|
|
153
153
|
}, async ({ memoryId, content, contentL1 }) => {
|
|
154
|
-
const existing = db.prepare(
|
|
154
|
+
const existing = db.prepare('SELECT id, core FROM memory WHERE id = ?').get(memoryId);
|
|
155
155
|
if (!existing)
|
|
156
156
|
throw new Error(`Memory #${memoryId} not found`);
|
|
157
157
|
if (!existing.core)
|
|
@@ -161,152 +161,181 @@ server.registerTool("memory_update_core", {
|
|
|
161
161
|
throw new ReferenceError('Unable to create query embedding');
|
|
162
162
|
}
|
|
163
163
|
queries.updateCoreMemory(db).run(content, contentL1, memoryId);
|
|
164
|
-
db.prepare(
|
|
165
|
-
db.prepare(
|
|
164
|
+
db.prepare('DELETE FROM vec_memories WHERE memory_id = ?').run(BigInt(memoryId));
|
|
165
|
+
db.prepare('INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)').run(BigInt(memoryId), Buffer.from(embedding.buffer));
|
|
166
166
|
return {
|
|
167
|
-
content: [{ type:
|
|
167
|
+
content: [{ type: 'text', text: `Updated core memory #${memoryId}` }],
|
|
168
168
|
};
|
|
169
169
|
});
|
|
170
|
-
server.registerTool(
|
|
171
|
-
description:
|
|
170
|
+
server.registerTool('memory_recent', {
|
|
171
|
+
description: 'Get recent memories within time window',
|
|
172
172
|
inputSchema: {
|
|
173
|
-
hours: z.number().optional().describe(
|
|
174
|
-
limit: z.number().optional().describe(
|
|
173
|
+
hours: z.number().optional().describe('Hours to look back (default: 24)'),
|
|
174
|
+
limit: z.number().optional().describe('Maximum results (default: 20)'),
|
|
175
175
|
},
|
|
176
176
|
}, async ({ hours = 24, limit = 20 }) => {
|
|
177
177
|
const results = queries.getRecentMemories(db).all(`-${hours} hours`, limit);
|
|
178
178
|
for (const result of results) {
|
|
179
|
-
logAccess(result.id,
|
|
179
|
+
logAccess(result.id, 'recent');
|
|
180
180
|
}
|
|
181
181
|
return {
|
|
182
|
-
content: [{ type:
|
|
182
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
183
183
|
};
|
|
184
184
|
});
|
|
185
|
-
server.registerTool(
|
|
186
|
-
description:
|
|
185
|
+
server.registerTool('memory_stats', {
|
|
186
|
+
description: 'Get memory database statistics',
|
|
187
187
|
inputSchema: {},
|
|
188
188
|
}, async () => {
|
|
189
189
|
const stats = queries.getStats(db).all();
|
|
190
|
-
const total = db.prepare(
|
|
191
|
-
const tags = db.prepare(
|
|
192
|
-
const relationships = db.prepare(
|
|
193
|
-
const accessLogs = db.prepare(
|
|
190
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM memory').get().count;
|
|
191
|
+
const tags = db.prepare('SELECT COUNT(*) as count FROM tag').get().count;
|
|
192
|
+
const relationships = db.prepare('SELECT COUNT(*) as count FROM relationship').get().count;
|
|
193
|
+
const accessLogs = db.prepare('SELECT COUNT(*) as count FROM access_log').get().count;
|
|
194
194
|
const rows = stats
|
|
195
195
|
.map((s) => ` ${s.type.padEnd(15)} ${s.count.toString().padStart(3)} memories (avg importance: ${s.avg_importance?.toFixed(1)})`)
|
|
196
|
-
.join(
|
|
196
|
+
.join('\n');
|
|
197
197
|
return {
|
|
198
198
|
content: [
|
|
199
199
|
{
|
|
200
|
-
type:
|
|
200
|
+
type: 'text',
|
|
201
201
|
text: `Memory Statistics:\n\n${rows}\n\n Total: ${total} memories\n Tags: ${tags}\n Relationships: ${relationships}\n Access Logs: ${accessLogs}`,
|
|
202
202
|
},
|
|
203
203
|
],
|
|
204
204
|
};
|
|
205
205
|
});
|
|
206
|
-
server.registerTool(
|
|
207
|
-
description:
|
|
206
|
+
server.registerTool('memory_consolidate', {
|
|
207
|
+
description: 'Run memory consolidation cycle (decay old, boost accessed)',
|
|
208
208
|
inputSchema: {},
|
|
209
209
|
}, async () => {
|
|
210
210
|
const results = runConsolidation();
|
|
211
211
|
return {
|
|
212
212
|
content: [
|
|
213
213
|
{
|
|
214
|
-
type:
|
|
214
|
+
type: 'text',
|
|
215
215
|
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`,
|
|
216
216
|
},
|
|
217
217
|
],
|
|
218
218
|
};
|
|
219
219
|
});
|
|
220
|
-
server.registerTool(
|
|
221
|
-
description:
|
|
220
|
+
server.registerTool('memory_relationships', {
|
|
221
|
+
description: 'View relationship graph for a memory',
|
|
222
222
|
inputSchema: {
|
|
223
|
-
memoryId: z.number().describe(
|
|
223
|
+
memoryId: z.number().describe('Memory ID to explore'),
|
|
224
224
|
},
|
|
225
225
|
}, async ({ memoryId }) => {
|
|
226
226
|
const relationships = queries.getMemoryRelationships(db).all(memoryId, memoryId);
|
|
227
227
|
return {
|
|
228
|
-
content: [{ type:
|
|
228
|
+
content: [{ type: 'text', text: JSON.stringify(relationships, null, 2) }],
|
|
229
229
|
};
|
|
230
230
|
});
|
|
231
|
-
server.registerTool(
|
|
232
|
-
description:
|
|
231
|
+
server.registerTool('memory_link', {
|
|
232
|
+
description: 'Explicitly create a typed relationship between two memories',
|
|
233
233
|
inputSchema: {
|
|
234
|
-
fromId: z.number().describe(
|
|
235
|
-
toId: z.number().describe(
|
|
236
|
-
type: z.enum([
|
|
237
|
-
.describe(
|
|
234
|
+
fromId: z.number().describe('Source memory ID'),
|
|
235
|
+
toId: z.number().describe('Target memory ID'),
|
|
236
|
+
type: z.enum(['related', 'supersedes', 'supports', 'contradicts', 'derives_from'])
|
|
237
|
+
.describe('Relationship type'),
|
|
238
238
|
},
|
|
239
239
|
}, async ({ fromId, toId, type }) => {
|
|
240
|
-
const from = db.prepare(
|
|
240
|
+
const from = db.prepare('SELECT id FROM memory WHERE id = ?').get(fromId);
|
|
241
241
|
if (!from)
|
|
242
242
|
throw new Error(`Memory #${fromId} not found`);
|
|
243
|
-
const to = db.prepare(
|
|
243
|
+
const to = db.prepare('SELECT id FROM memory WHERE id = ?').get(toId);
|
|
244
244
|
if (!to)
|
|
245
245
|
throw new Error(`Memory #${toId} not found`);
|
|
246
246
|
const existing = queries.findRelationshipBetween(db).get(fromId, toId, type);
|
|
247
247
|
if (existing) {
|
|
248
|
-
return { content: [{ type:
|
|
248
|
+
return { content: [{ type: 'text', text: `Relationship already exists (id: ${existing.id})` }] };
|
|
249
249
|
}
|
|
250
250
|
const result = queries.insertRelationship(db).run(fromId, toId, type);
|
|
251
251
|
const id = Number(result.lastInsertRowid);
|
|
252
252
|
return {
|
|
253
|
-
content: [{ type:
|
|
253
|
+
content: [{ type: 'text', text: `Created relationship #${id}: #${fromId} -[${type}]-> #${toId}` }],
|
|
254
254
|
};
|
|
255
255
|
});
|
|
256
|
-
server.registerTool(
|
|
257
|
-
description:
|
|
256
|
+
server.registerTool('memory_unlink', {
|
|
257
|
+
description: 'Remove a relationship between two memories by relationship ID',
|
|
258
258
|
inputSchema: {
|
|
259
|
-
relationshipId: z.number().describe(
|
|
259
|
+
relationshipId: z.number().describe('Relationship ID to delete'),
|
|
260
260
|
},
|
|
261
261
|
}, async ({ relationshipId }) => {
|
|
262
|
-
const existing = db.prepare(
|
|
262
|
+
const existing = db.prepare('SELECT id FROM relationship WHERE id = ?').get(relationshipId);
|
|
263
263
|
if (!existing)
|
|
264
264
|
throw new Error(`Relationship #${relationshipId} not found`);
|
|
265
265
|
queries.deleteRelationship(db).run(relationshipId);
|
|
266
266
|
return {
|
|
267
|
-
content: [{ type:
|
|
267
|
+
content: [{ type: 'text', text: `Deleted relationship #${relationshipId}` }],
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
server.registerTool('memory_supersede', {
|
|
271
|
+
description: 'Mark an existing memory as superseded by another — use for deduplication. Demotes the duplicate\'s importance and links it to the canonical memory. Refuses core memories.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
duplicateId: z.number().describe('ID of the duplicate memory to demote'),
|
|
274
|
+
canonicalId: z.number().describe('ID of the canonical memory to keep'),
|
|
275
|
+
},
|
|
276
|
+
}, async ({ duplicateId, canonicalId }) => {
|
|
277
|
+
if (duplicateId === canonicalId)
|
|
278
|
+
throw new Error('duplicate and canonical must be different memories');
|
|
279
|
+
const dup = db.prepare('SELECT id, core FROM memory WHERE id = ?').get(duplicateId);
|
|
280
|
+
if (!dup)
|
|
281
|
+
throw new Error(`Memory #${duplicateId} not found`);
|
|
282
|
+
if (dup.core)
|
|
283
|
+
throw new Error(`Memory #${duplicateId} is a core memory and cannot be superseded — use memory_update_core instead`);
|
|
284
|
+
const can = db.prepare('SELECT id FROM memory WHERE id = ?').get(canonicalId);
|
|
285
|
+
if (!can)
|
|
286
|
+
throw new Error(`Canonical memory #${canonicalId} not found`);
|
|
287
|
+
const alreadyLinked = db.prepare('SELECT supersedes_id FROM memory WHERE id = ? AND supersedes_id IS NOT NULL').get(duplicateId);
|
|
288
|
+
if (alreadyLinked)
|
|
289
|
+
throw new Error(`Memory #${duplicateId} already has a supersedes_id — check memory_supersede_chain first`);
|
|
290
|
+
db.prepare('UPDATE memory SET supersedes_id = ?, importance = MAX(1, importance - 2), updated_at = datetime(\'now\') WHERE id = ?').run(canonicalId, duplicateId);
|
|
291
|
+
const existing = queries.findRelationshipBetween(db).get(duplicateId, canonicalId, 'supersedes');
|
|
292
|
+
if (!existing) {
|
|
293
|
+
queries.insertRelationship(db).run(duplicateId, canonicalId, 'supersedes');
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: 'text', text: `Memory #${duplicateId} now superseded by #${canonicalId} (importance demoted by 2)` }],
|
|
268
297
|
};
|
|
269
298
|
});
|
|
270
299
|
// ---------------------------------------------------------------------------
|
|
271
300
|
// journal tools
|
|
272
301
|
// ---------------------------------------------------------------------------
|
|
273
|
-
server.registerTool(
|
|
274
|
-
description:
|
|
302
|
+
server.registerTool('journal_add', {
|
|
303
|
+
description: 'Add a daily journal entry',
|
|
275
304
|
inputSchema: {
|
|
276
|
-
date: z.string().describe(
|
|
277
|
-
content: z.string().describe(
|
|
278
|
-
learned: z.string().optional().describe(
|
|
279
|
-
keyMoments: z.string().optional().describe(
|
|
280
|
-
mood: z.string().optional().describe(
|
|
305
|
+
date: z.string().describe('Date YYYY-MM-DD'),
|
|
306
|
+
content: z.string().describe('Full journal narrative'),
|
|
307
|
+
learned: z.string().optional().describe('What I learned (optional)'),
|
|
308
|
+
keyMoments: z.string().optional().describe('Key moments (optional)'),
|
|
309
|
+
mood: z.string().optional().describe('Mood/feeling (optional)'),
|
|
281
310
|
},
|
|
282
311
|
}, async ({ date, content, learned, keyMoments, mood }) => {
|
|
283
|
-
const combinedLearned = [learned].filter(Boolean).join(
|
|
312
|
+
const combinedLearned = [learned].filter(Boolean).join('\n\n') || null;
|
|
284
313
|
queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
|
|
285
314
|
return {
|
|
286
|
-
content: [{ type:
|
|
315
|
+
content: [{ type: 'text', text: `Journal entry created for ${date}` }],
|
|
287
316
|
};
|
|
288
317
|
});
|
|
289
|
-
server.registerTool(
|
|
290
|
-
description:
|
|
318
|
+
server.registerTool('journal_recent', {
|
|
319
|
+
description: 'Get recent journal entries',
|
|
291
320
|
inputSchema: {
|
|
292
|
-
days: z.number().optional().describe(
|
|
321
|
+
days: z.number().optional().describe('Days to look back (default: 7)'),
|
|
293
322
|
},
|
|
294
323
|
}, async ({ days = 7 }) => {
|
|
295
324
|
const entries = queries.getRecentJournals(db).all(`-${days} days`);
|
|
296
325
|
return {
|
|
297
|
-
content: [{ type:
|
|
326
|
+
content: [{ type: 'text', text: JSON.stringify(entries, null, 2) }],
|
|
298
327
|
};
|
|
299
328
|
});
|
|
300
329
|
// ---------------------------------------------------------------------------
|
|
301
330
|
// task tools
|
|
302
331
|
// ---------------------------------------------------------------------------
|
|
303
|
-
server.registerTool(
|
|
304
|
-
description:
|
|
332
|
+
server.registerTool('task_create', {
|
|
333
|
+
description: 'Create a new task. Use session_id to group tasks by project or feature.',
|
|
305
334
|
inputSchema: {
|
|
306
|
-
title: z.string().describe(
|
|
307
|
-
description: z.string().optional().describe(
|
|
335
|
+
title: z.string().describe('Short task title'),
|
|
336
|
+
description: z.string().optional().describe('Detailed description of what needs to be done'),
|
|
308
337
|
session_id: z.string().optional().describe("Project or feature label to group related tasks (e.g. 'postnesia-mcp', 'auth-refactor')"),
|
|
309
|
-
memory_id: z.number().optional().describe(
|
|
338
|
+
memory_id: z.number().optional().describe('Optional ID of a related memory'),
|
|
310
339
|
},
|
|
311
340
|
}, async ({ title, description, session_id, memory_id }) => {
|
|
312
341
|
const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
|
|
@@ -314,19 +343,19 @@ server.registerTool("task_create", {
|
|
|
314
343
|
return {
|
|
315
344
|
content: [
|
|
316
345
|
{
|
|
317
|
-
type:
|
|
318
|
-
text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` :
|
|
346
|
+
type: 'text',
|
|
347
|
+
text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ''}`,
|
|
319
348
|
},
|
|
320
349
|
],
|
|
321
350
|
};
|
|
322
351
|
});
|
|
323
|
-
server.registerTool(
|
|
352
|
+
server.registerTool('task_update', {
|
|
324
353
|
description: "Update a task's status, title, or description",
|
|
325
354
|
inputSchema: {
|
|
326
|
-
taskId: z.number().describe(
|
|
327
|
-
status: z.enum([
|
|
328
|
-
title: z.string().optional().describe(
|
|
329
|
-
description: z.string().optional().describe(
|
|
355
|
+
taskId: z.number().describe('ID of the task to update'),
|
|
356
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).optional().describe('New status'),
|
|
357
|
+
title: z.string().optional().describe('Updated title'),
|
|
358
|
+
description: z.string().optional().describe('Updated description'),
|
|
330
359
|
},
|
|
331
360
|
}, async ({ taskId, status, title, description }) => {
|
|
332
361
|
const existing = queries.getTaskById(db).get(taskId);
|
|
@@ -337,31 +366,31 @@ server.registerTool("task_update", {
|
|
|
337
366
|
return {
|
|
338
367
|
content: [
|
|
339
368
|
{
|
|
340
|
-
type:
|
|
369
|
+
type: 'text',
|
|
341
370
|
text: `Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
|
|
342
371
|
},
|
|
343
372
|
],
|
|
344
373
|
};
|
|
345
374
|
});
|
|
346
|
-
server.registerTool(
|
|
347
|
-
description:
|
|
375
|
+
server.registerTool('task_list', {
|
|
376
|
+
description: 'List tasks, optionally filtered by status and/or session_id. Use at session start to resume open work.',
|
|
348
377
|
inputSchema: {
|
|
349
|
-
status: z.enum([
|
|
350
|
-
session_id: z.string().optional().describe(
|
|
351
|
-
limit: z.number().optional().describe(
|
|
378
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).optional().describe('Filter by status'),
|
|
379
|
+
session_id: z.string().optional().describe('Filter by project/feature label'),
|
|
380
|
+
limit: z.number().optional().describe('Maximum results (default: 50)'),
|
|
352
381
|
},
|
|
353
382
|
}, async ({ status, session_id, limit = 50 }) => {
|
|
354
383
|
const conditions = [];
|
|
355
384
|
const params = [];
|
|
356
385
|
if (status) {
|
|
357
|
-
conditions.push(
|
|
386
|
+
conditions.push('status = ?');
|
|
358
387
|
params.push(status);
|
|
359
388
|
}
|
|
360
389
|
if (session_id) {
|
|
361
|
-
conditions.push(
|
|
390
|
+
conditions.push('session_id = ?');
|
|
362
391
|
params.push(session_id);
|
|
363
392
|
}
|
|
364
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(
|
|
393
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
365
394
|
params.push(limit);
|
|
366
395
|
const tasks = db.prepare(`
|
|
367
396
|
SELECT * FROM task
|
|
@@ -370,7 +399,7 @@ server.registerTool("task_list", {
|
|
|
370
399
|
LIMIT ?
|
|
371
400
|
`).all(...params);
|
|
372
401
|
return {
|
|
373
|
-
content: [{ type:
|
|
402
|
+
content: [{ type: 'text', text: JSON.stringify(tasks, null, 2) }],
|
|
374
403
|
};
|
|
375
404
|
});
|
|
376
405
|
// ---------------------------------------------------------------------------
|
|
@@ -379,12 +408,16 @@ server.registerTool("task_list", {
|
|
|
379
408
|
async function main() {
|
|
380
409
|
const transport = new StdioServerTransport();
|
|
381
410
|
await server.connect(transport);
|
|
382
|
-
console.error(
|
|
411
|
+
console.error('Postnesia MCP Server running on stdio');
|
|
383
412
|
}
|
|
413
|
+
const exit = () => {
|
|
414
|
+
closeDb();
|
|
415
|
+
process.exit(0);
|
|
416
|
+
};
|
|
384
417
|
main().catch((error) => {
|
|
385
|
-
console.error(
|
|
418
|
+
console.error('Fatal error:', error);
|
|
386
419
|
closeDb();
|
|
387
420
|
process.exit(1);
|
|
388
421
|
});
|
|
389
|
-
process.on('SIGINT',
|
|
390
|
-
process.on('SIGTERM',
|
|
422
|
+
process.on('SIGINT', exit);
|
|
423
|
+
process.on('SIGTERM', exit);
|
package/dist/install.js
CHANGED
|
@@ -24,11 +24,11 @@ const config = {
|
|
|
24
24
|
claude: {
|
|
25
25
|
mcpServers: {
|
|
26
26
|
postnesia: {
|
|
27
|
-
type:
|
|
27
|
+
type: 'stdio',
|
|
28
28
|
enabled: true,
|
|
29
|
-
command:
|
|
29
|
+
command: 'npx',
|
|
30
30
|
args: [
|
|
31
|
-
|
|
31
|
+
'pn-mcp'
|
|
32
32
|
],
|
|
33
33
|
env: {
|
|
34
34
|
DATABASE_URL: process.env['DATABASE_URL'],
|
|
@@ -40,11 +40,11 @@ const config = {
|
|
|
40
40
|
opencode: {
|
|
41
41
|
mcp: {
|
|
42
42
|
postnesia: {
|
|
43
|
-
type:
|
|
43
|
+
type: 'local',
|
|
44
44
|
enabled: false,
|
|
45
45
|
command: [
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
'npx',
|
|
47
|
+
'pn-mcp'
|
|
48
48
|
],
|
|
49
49
|
environment: {
|
|
50
50
|
DATABASE_URL: process.env['DATABASE_URL'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postnesia/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "An MCP server to interact with the Postnesia database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
16
16
|
"zod": "^4.3.6",
|
|
17
|
-
"@postnesia/db": "0.1.
|
|
17
|
+
"@postnesia/db": "0.1.16"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^
|
|
20
|
+
"@types/node": "^24.12.0",
|
|
21
21
|
"dotenv": "^17.3.1",
|
|
22
22
|
"tsx": "^4.19.2",
|
|
23
23
|
"typescript": "^5.7.3"
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "pnpm clean && tsc -p tsconfig.json",
|
|
27
27
|
"clean": "rm -rf ./dist",
|
|
28
|
-
"start": "tsx src/index.ts"
|
|
28
|
+
"start": "tsx src/index.ts",
|
|
29
|
+
"lint": "pnpm eslint .",
|
|
30
|
+
"lint:fix": "pnpm eslint --fix ."
|
|
29
31
|
}
|
|
30
32
|
}
|