@postnesia/mcp 0.1.14 → 0.1.15
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 +141 -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,152 @@ 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
268
|
};
|
|
269
269
|
});
|
|
270
270
|
// ---------------------------------------------------------------------------
|
|
271
271
|
// journal tools
|
|
272
272
|
// ---------------------------------------------------------------------------
|
|
273
|
-
server.registerTool(
|
|
274
|
-
description:
|
|
273
|
+
server.registerTool('journal_add', {
|
|
274
|
+
description: 'Add a daily journal entry',
|
|
275
275
|
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(
|
|
276
|
+
date: z.string().describe('Date YYYY-MM-DD'),
|
|
277
|
+
content: z.string().describe('Full journal narrative'),
|
|
278
|
+
learned: z.string().optional().describe('What I learned (optional)'),
|
|
279
|
+
keyMoments: z.string().optional().describe('Key moments (optional)'),
|
|
280
|
+
mood: z.string().optional().describe('Mood/feeling (optional)'),
|
|
281
281
|
},
|
|
282
282
|
}, async ({ date, content, learned, keyMoments, mood }) => {
|
|
283
|
-
const combinedLearned = [learned].filter(Boolean).join(
|
|
283
|
+
const combinedLearned = [learned].filter(Boolean).join('\n\n') || null;
|
|
284
284
|
queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
|
|
285
285
|
return {
|
|
286
|
-
content: [{ type:
|
|
286
|
+
content: [{ type: 'text', text: `Journal entry created for ${date}` }],
|
|
287
287
|
};
|
|
288
288
|
});
|
|
289
|
-
server.registerTool(
|
|
290
|
-
description:
|
|
289
|
+
server.registerTool('journal_recent', {
|
|
290
|
+
description: 'Get recent journal entries',
|
|
291
291
|
inputSchema: {
|
|
292
|
-
days: z.number().optional().describe(
|
|
292
|
+
days: z.number().optional().describe('Days to look back (default: 7)'),
|
|
293
293
|
},
|
|
294
294
|
}, async ({ days = 7 }) => {
|
|
295
295
|
const entries = queries.getRecentJournals(db).all(`-${days} days`);
|
|
296
296
|
return {
|
|
297
|
-
content: [{ type:
|
|
297
|
+
content: [{ type: 'text', text: JSON.stringify(entries, null, 2) }],
|
|
298
298
|
};
|
|
299
299
|
});
|
|
300
300
|
// ---------------------------------------------------------------------------
|
|
301
301
|
// task tools
|
|
302
302
|
// ---------------------------------------------------------------------------
|
|
303
|
-
server.registerTool(
|
|
304
|
-
description:
|
|
303
|
+
server.registerTool('task_create', {
|
|
304
|
+
description: 'Create a new task. Use session_id to group tasks by project or feature.',
|
|
305
305
|
inputSchema: {
|
|
306
|
-
title: z.string().describe(
|
|
307
|
-
description: z.string().optional().describe(
|
|
306
|
+
title: z.string().describe('Short task title'),
|
|
307
|
+
description: z.string().optional().describe('Detailed description of what needs to be done'),
|
|
308
308
|
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(
|
|
309
|
+
memory_id: z.number().optional().describe('Optional ID of a related memory'),
|
|
310
310
|
},
|
|
311
311
|
}, async ({ title, description, session_id, memory_id }) => {
|
|
312
312
|
const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
|
|
@@ -314,19 +314,19 @@ server.registerTool("task_create", {
|
|
|
314
314
|
return {
|
|
315
315
|
content: [
|
|
316
316
|
{
|
|
317
|
-
type:
|
|
318
|
-
text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` :
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ''}`,
|
|
319
319
|
},
|
|
320
320
|
],
|
|
321
321
|
};
|
|
322
322
|
});
|
|
323
|
-
server.registerTool(
|
|
323
|
+
server.registerTool('task_update', {
|
|
324
324
|
description: "Update a task's status, title, or description",
|
|
325
325
|
inputSchema: {
|
|
326
|
-
taskId: z.number().describe(
|
|
327
|
-
status: z.enum([
|
|
328
|
-
title: z.string().optional().describe(
|
|
329
|
-
description: z.string().optional().describe(
|
|
326
|
+
taskId: z.number().describe('ID of the task to update'),
|
|
327
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).optional().describe('New status'),
|
|
328
|
+
title: z.string().optional().describe('Updated title'),
|
|
329
|
+
description: z.string().optional().describe('Updated description'),
|
|
330
330
|
},
|
|
331
331
|
}, async ({ taskId, status, title, description }) => {
|
|
332
332
|
const existing = queries.getTaskById(db).get(taskId);
|
|
@@ -337,31 +337,31 @@ server.registerTool("task_update", {
|
|
|
337
337
|
return {
|
|
338
338
|
content: [
|
|
339
339
|
{
|
|
340
|
-
type:
|
|
340
|
+
type: 'text',
|
|
341
341
|
text: `Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
|
|
342
342
|
},
|
|
343
343
|
],
|
|
344
344
|
};
|
|
345
345
|
});
|
|
346
|
-
server.registerTool(
|
|
347
|
-
description:
|
|
346
|
+
server.registerTool('task_list', {
|
|
347
|
+
description: 'List tasks, optionally filtered by status and/or session_id. Use at session start to resume open work.',
|
|
348
348
|
inputSchema: {
|
|
349
|
-
status: z.enum([
|
|
350
|
-
session_id: z.string().optional().describe(
|
|
351
|
-
limit: z.number().optional().describe(
|
|
349
|
+
status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']).optional().describe('Filter by status'),
|
|
350
|
+
session_id: z.string().optional().describe('Filter by project/feature label'),
|
|
351
|
+
limit: z.number().optional().describe('Maximum results (default: 50)'),
|
|
352
352
|
},
|
|
353
353
|
}, async ({ status, session_id, limit = 50 }) => {
|
|
354
354
|
const conditions = [];
|
|
355
355
|
const params = [];
|
|
356
356
|
if (status) {
|
|
357
|
-
conditions.push(
|
|
357
|
+
conditions.push('status = ?');
|
|
358
358
|
params.push(status);
|
|
359
359
|
}
|
|
360
360
|
if (session_id) {
|
|
361
|
-
conditions.push(
|
|
361
|
+
conditions.push('session_id = ?');
|
|
362
362
|
params.push(session_id);
|
|
363
363
|
}
|
|
364
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(
|
|
364
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
365
365
|
params.push(limit);
|
|
366
366
|
const tasks = db.prepare(`
|
|
367
367
|
SELECT * FROM task
|
|
@@ -370,7 +370,7 @@ server.registerTool("task_list", {
|
|
|
370
370
|
LIMIT ?
|
|
371
371
|
`).all(...params);
|
|
372
372
|
return {
|
|
373
|
-
content: [{ type:
|
|
373
|
+
content: [{ type: 'text', text: JSON.stringify(tasks, null, 2) }],
|
|
374
374
|
};
|
|
375
375
|
});
|
|
376
376
|
// ---------------------------------------------------------------------------
|
|
@@ -379,12 +379,16 @@ server.registerTool("task_list", {
|
|
|
379
379
|
async function main() {
|
|
380
380
|
const transport = new StdioServerTransport();
|
|
381
381
|
await server.connect(transport);
|
|
382
|
-
console.error(
|
|
382
|
+
console.error('Postnesia MCP Server running on stdio');
|
|
383
383
|
}
|
|
384
|
+
const exit = () => {
|
|
385
|
+
closeDb();
|
|
386
|
+
process.exit(0);
|
|
387
|
+
};
|
|
384
388
|
main().catch((error) => {
|
|
385
|
-
console.error(
|
|
389
|
+
console.error('Fatal error:', error);
|
|
386
390
|
closeDb();
|
|
387
391
|
process.exit(1);
|
|
388
392
|
});
|
|
389
|
-
process.on('SIGINT',
|
|
390
|
-
process.on('SIGTERM',
|
|
393
|
+
process.on('SIGINT', exit);
|
|
394
|
+
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.15",
|
|
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.15"
|
|
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
|
}
|