@postnesia/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/dist/access.d.ts +22 -0
- package/dist/access.js +63 -0
- package/dist/importance.d.ts +44 -0
- package/dist/importance.js +145 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +514 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Postnesia Memory MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for main agent memory system. Exposes memory operations as standardized tools.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
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):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"postnesia": {
|
|
21
|
+
"command": "/absolute/path/to/tsx",
|
|
22
|
+
"env": {
|
|
23
|
+
"DATABASE_URL": "file:/absolute/path/to/memory.db",
|
|
24
|
+
"GEMINI_API_KEY": "token-for-gemini-embedding-model-api"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or use npm script:
|
|
32
|
+
|
|
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)
|
|
68
|
+
|
|
69
|
+
### memory_context
|
|
70
|
+
Get contextually related memories.
|
|
71
|
+
- `query` (required): Context query
|
|
72
|
+
- `maxResults` (optional): Max results (default: 5)
|
|
73
|
+
|
|
74
|
+
### memory_stats
|
|
75
|
+
Get database statistics. No parameters.
|
|
76
|
+
|
|
77
|
+
### memory_consolidate
|
|
78
|
+
Run consolidation cycle (decay + boost). No parameters - always applies changes.
|
|
79
|
+
|
|
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
|
|
88
|
+
|
|
89
|
+
### journal_recent
|
|
90
|
+
Get recent journal entries.
|
|
91
|
+
- `days` (optional): Days to look back (default: 7)
|
|
92
|
+
|
|
93
|
+
### memory_relationships
|
|
94
|
+
View relationship graph for a memory.
|
|
95
|
+
- `memoryId` (required): Memory ID to explore
|
|
96
|
+
|
|
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
|
+
```
|
|
108
|
+
|
|
109
|
+
Then send MCP requests via stdin (see MCP protocol docs).
|
|
110
|
+
|
|
111
|
+
## Architecture
|
|
112
|
+
|
|
113
|
+
- Built on Prisma + SQLite
|
|
114
|
+
- Automatic access tracking
|
|
115
|
+
- Dynamic importance scoring (decay + boost)
|
|
116
|
+
- Relationship graph support
|
|
117
|
+
- Journal integration
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for Belle's Memory System
|
|
4
|
+
* Exposes memory operations as Model Context Protocol tools
|
|
5
|
+
*/
|
|
6
|
+
import 'dotenv/config';
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { getDb, queries, createMemory } from "@postnesia/db";
|
|
11
|
+
import { embed } from "@postnesia/db/embeddings";
|
|
12
|
+
import { logAccess } from "./access.js";
|
|
13
|
+
import { runConsolidation } from "./importance.js";
|
|
14
|
+
const db = getDb();
|
|
15
|
+
const server = new Server({
|
|
16
|
+
name: "openmind",
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
}, {
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
// Tool definitions
|
|
24
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
25
|
+
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
|
+
},
|
|
174
|
+
{
|
|
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
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
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
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
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
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
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
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
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
|
+
},
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
});
|
|
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
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: `Error: ${error.message}`,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
isError: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
// Start server
|
|
508
|
+
try {
|
|
509
|
+
const transport = new StdioServerTransport();
|
|
510
|
+
await server.connect(transport);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
console.error("OpenMind Memory MCP Server running on stdio");
|
|
514
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@postnesia/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An MCP server to interact with the Postnesia database",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"postnesia-mcp": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
14
|
+
"@postnesia/db": "0.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.10.5",
|
|
18
|
+
"tsx": "^4.19.2",
|
|
19
|
+
"typescript": "^5.7.3"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"start": "tsx src/index.ts"
|
|
24
|
+
}
|
|
25
|
+
}
|