@postnesia/mcp 0.1.4 → 0.1.5
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 +37 -91
- package/dist/index.d.ts +2 -2
- package/dist/index.js +243 -482
- package/package.json +4 -12
package/README.md
CHANGED
|
@@ -1,117 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @postnesia/mcp
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
|
-
|
|
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": "
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["postnesia-mcp"],
|
|
22
15
|
"env": {
|
|
23
|
-
"DATABASE_URL": "file
|
|
24
|
-
"GEMINI_API_KEY": "
|
|
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
|
-
|
|
26
|
+
`DATABASE_URL` must be an absolute `file://` URL.
|
|
32
27
|
|
|
33
|
-
|
|
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
|
-
###
|
|
70
|
-
Get contextually related memories.
|
|
71
|
-
- `query` (required): Context query
|
|
72
|
-
- `maxResults` (optional): Max results (default: 5)
|
|
30
|
+
### Memory
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
Run consolidation cycle (decay + boost). No parameters - always applies changes.
|
|
42
|
+
**Memory types:** `event` `decision` `lesson` `preference` `person` `technical`
|
|
79
43
|
|
|
80
|
-
###
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
###
|
|
94
|
-
View relationship graph for a memory.
|
|
95
|
-
- `memoryId` (required): Memory ID to explore
|
|
51
|
+
### Tasks
|
|
96
52
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
59
|
+
**Task statuses:** `pending` `in_progress` `completed` `cancelled`
|
|
110
60
|
|
|
111
|
-
##
|
|
61
|
+
## Implementation
|
|
112
62
|
|
|
113
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -1,514 +1,275 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Server for
|
|
3
|
+
* MCP Server for Postnesia Memory System
|
|
4
4
|
* Exposes memory operations as Model Context Protocol tools
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
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 {
|
|
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 "
|
|
13
|
-
import { runConsolidation } from "
|
|
11
|
+
import { logAccess } from "@postnesia/db/access";
|
|
12
|
+
import { runConsolidation } from "@postnesia/db/importance";
|
|
14
13
|
const db = getDb();
|
|
15
|
-
const server = new
|
|
16
|
-
name: "
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "postnesia",
|
|
17
16
|
version: "1.0.0",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
|
513
|
-
console.error("
|
|
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.
|
|
3
|
+
"version": "0.1.5",
|
|
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.
|
|
15
|
+
"@postnesia/db": "^0.1.5",
|
|
16
|
+
"zod": "^4.3.6"
|
|
25
17
|
},
|
|
26
18
|
"devDependencies": {
|
|
27
19
|
"@types/node": "^22.10.5",
|