@iwo-szapar/data-mcp 0.2.1 → 0.3.2
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 +286 -0
- package/dist/server.js +2 -2
- package/dist/tools/memory/link-create.d.ts +4 -0
- package/dist/tools/memory/link-create.js +67 -0
- package/dist/tools/memory/link-delete.d.ts +4 -0
- package/dist/tools/memory/link-delete.js +27 -0
- package/dist/tools/memory/link-related.d.ts +4 -0
- package/dist/tools/memory/link-related.js +91 -0
- package/dist/tools/memory/link-suggest.d.ts +4 -0
- package/dist/tools/memory/link-suggest.js +83 -0
- package/dist/tools/register.js +10 -2
- package/migrations/pocketbase/001_core_schema.js +28 -24
- package/migrations/pocketbase/002_goals_tasks.js +18 -15
- package/migrations/pocketbase/003_contacts.js +12 -9
- package/migrations/pocketbase/004_entity_aliases.js +12 -9
- package/migrations/pocketbase/005_prospects.js +14 -11
- package/migrations/pocketbase/006_business.js +33 -29
- package/migrations/pocketbase/007_newsletter_affiliates.js +19 -16
- package/migrations/pocketbase/008_knowledge_links.js +41 -0
- package/migrations/supabase/009_align_to_production.sql +11 -14
- package/migrations/supabase/010_knowledge_links.sql +47 -0
- package/package.json +20 -5
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# @iwo-szapar/data-mcp
|
|
2
|
+
|
|
3
|
+
Unified data MCP server for [Second Brain](https://iwoszapar.com/second-brain-ai). One MCP, two backends: PocketBase (local, free) or Supabase (cloud, multi-device).
|
|
4
|
+
|
|
5
|
+
40 tools across knowledge, sessions, goals, tasks, contacts, CRM prospects, blog, email queue, and content calendar. Used in production by Second Brain v2 customers.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @iwo-szapar/data-mcp
|
|
13
|
+
# or run on demand
|
|
14
|
+
npx @iwo-szapar/data-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js `>=20`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick start — PocketBase (local)
|
|
22
|
+
|
|
23
|
+
PocketBase runs on your laptop. Good for single-device workflows. Stops when you close the terminal.
|
|
24
|
+
|
|
25
|
+
1. **Install PocketBase** ([pocketbase.io](https://pocketbase.io)) and start it:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
./pocketbase serve
|
|
29
|
+
# Admin UI: http://127.0.0.1:8090/_/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. **Create an admin account** via the Admin UI on first run.
|
|
33
|
+
|
|
34
|
+
3. **Apply the schema migrations** (required — the MCP does NOT apply them automatically):
|
|
35
|
+
|
|
36
|
+
Copy the files in `migrations/pocketbase/` (shipped with this package) into your PocketBase instance's `pb_migrations/` directory, then run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
./pocketbase migrate up
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This creates all 14 collections (`knowledge`, `decisions`, `sessions`, `goals`, `tasks`, `contacts`, `entity_aliases`, `settings`, `prospects`, `blog_posts`, `email_queue`, `content_calendar`, `newsletter_subscribers`, `affiliates`).
|
|
43
|
+
|
|
44
|
+
4. **Configure your MCP client** (Claude Code, Claude Desktop, Cursor, etc.):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"data-mcp": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@iwo-szapar/data-mcp"],
|
|
52
|
+
"env": {
|
|
53
|
+
"SB_BACKEND": "pocketbase",
|
|
54
|
+
"SB_POCKETBASE_URL": "http://127.0.0.1:8090",
|
|
55
|
+
"SB_POCKETBASE_ADMIN_EMAIL": "you@example.com",
|
|
56
|
+
"SB_POCKETBASE_ADMIN_PASSWORD": "your-admin-password"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
5. **Verify**: in your MCP client, call the `setup_status` tool. It reports which collections exist and flags any missing ones.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start — Supabase (cloud, multi-device)
|
|
68
|
+
|
|
69
|
+
Supabase is a hosted Postgres. Runs 24/7, reachable from any device. Good for multi-device setups and phone-friendly workflows.
|
|
70
|
+
|
|
71
|
+
1. **Create a Supabase project** at [supabase.com](https://supabase.com). Note the Project URL and `service_role` key (Settings → API).
|
|
72
|
+
|
|
73
|
+
2. **Apply the SQL migrations** via the SQL editor or the Supabase CLI:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Using the Supabase CLI
|
|
77
|
+
for f in migrations/supabase/*.sql; do
|
|
78
|
+
psql "$SUPABASE_DB_URL" -f "$f"
|
|
79
|
+
done
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Apply them in order `001` through `010`. The MCP does NOT apply them automatically.
|
|
83
|
+
|
|
84
|
+
3. **Configure your MCP client**:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"data-mcp": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "@iwo-szapar/data-mcp"],
|
|
92
|
+
"env": {
|
|
93
|
+
"SB_BACKEND": "supabase",
|
|
94
|
+
"SB_SUPABASE_URL": "https://YOUR_PROJECT.supabase.co",
|
|
95
|
+
"SB_SUPABASE_KEY": "your-service-role-key"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use the `service_role` key, not `anon`. The MCP needs full access.
|
|
103
|
+
|
|
104
|
+
4. **Verify** with `setup_status`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Environment variables
|
|
109
|
+
|
|
110
|
+
| Variable | Required | Applies to | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `SB_BACKEND` | yes | both | `pocketbase` or `supabase` |
|
|
113
|
+
| `SB_POCKETBASE_URL` | yes (PB) | pocketbase | e.g. `http://127.0.0.1:8090` |
|
|
114
|
+
| `SB_POCKETBASE_ADMIN_EMAIL` | yes (PB) | pocketbase | PocketBase admin email |
|
|
115
|
+
| `SB_POCKETBASE_ADMIN_PASSWORD` | yes (PB) | pocketbase | PocketBase admin password |
|
|
116
|
+
| `SB_SUPABASE_URL` | yes (SB) | supabase | Project URL |
|
|
117
|
+
| `SB_SUPABASE_KEY` | yes (SB) | supabase | `service_role` key |
|
|
118
|
+
| `SB_SCHEMA_MAP` | no | both | JSON object mapping logical names to real table names (e.g. `{"prospects":"my_leads"}`) |
|
|
119
|
+
| `SB_RESEND_API_KEY` | no | both | Resend key for email tooling (optional) |
|
|
120
|
+
|
|
121
|
+
Missing any required var on startup → the server exits with `Missing required environment variable: SB_XXX`.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Tool reference (40 tools)
|
|
126
|
+
|
|
127
|
+
All tools return JSON. Every tool uses *graceful degradation*: if the required table doesn't exist, the tool returns a clear error asking you to apply migrations instead of crashing.
|
|
128
|
+
|
|
129
|
+
### Knowledge (8)
|
|
130
|
+
|
|
131
|
+
| Tool | Purpose |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `knowledge_store` | Store a fact / pattern / insight / lesson / reference. Dedup by `(type, title)`. |
|
|
134
|
+
| `knowledge_recall` | Search knowledge by query, tags, or type. |
|
|
135
|
+
| `knowledge_learn` | Shortcut for storing a `lesson`. |
|
|
136
|
+
| `knowledge_decide` | Record a decision with context, options, chosen option, and rationale (writes to `decisions`). |
|
|
137
|
+
| `knowledge_validate` | Mark an item as freshly validated (resets `last_validated_at`). |
|
|
138
|
+
| `knowledge_update` | Update title / content / tags on an existing item. |
|
|
139
|
+
| `knowledge_delete` | Delete a knowledge item by ID. |
|
|
140
|
+
| `knowledge_list` | List or filter knowledge items. |
|
|
141
|
+
|
|
142
|
+
### Sessions (2)
|
|
143
|
+
|
|
144
|
+
| Tool | Purpose |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `session_log` | Log a completed work session with skills used, files changed, decisions made. |
|
|
147
|
+
| `session_list` | List recent sessions. |
|
|
148
|
+
|
|
149
|
+
### Goals (3)
|
|
150
|
+
|
|
151
|
+
| Tool | Purpose |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `goal_create` / `goal_update` / `goal_list` | Track goals with key results. |
|
|
154
|
+
|
|
155
|
+
### Tasks (3)
|
|
156
|
+
|
|
157
|
+
| Tool | Purpose |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `task_create` / `task_update` / `task_list` | Task management with status and priority. |
|
|
160
|
+
|
|
161
|
+
### Contacts (4)
|
|
162
|
+
|
|
163
|
+
| Tool | Purpose |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `contact_create` / `contact_update` / `contact_list` / `contact_search` | Contact records with relationship type and tags. |
|
|
166
|
+
|
|
167
|
+
### Brain health (2)
|
|
168
|
+
|
|
169
|
+
| Tool | Purpose |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `brain_stats` | Aggregate counts across collections and knowledge-type distribution. |
|
|
172
|
+
| `brain_decay` | Find stale knowledge items (not validated recently). |
|
|
173
|
+
|
|
174
|
+
### Knowledge links (4)
|
|
175
|
+
|
|
176
|
+
| Tool | Purpose |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `link_create` / `link_delete` / `link_related` / `link_suggest` | Graph-lite relationships between knowledge items. |
|
|
179
|
+
|
|
180
|
+
### Setup (3)
|
|
181
|
+
|
|
182
|
+
| Tool | Purpose |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `setup_status` | Report which collections exist. **Run this first** after installation. |
|
|
185
|
+
| `setup_migrate` | **Reports** missing collections and points to the migration files. Does **not** apply migrations automatically — you must run them via PocketBase CLI or `psql`. |
|
|
186
|
+
| `setup_seed` | Load seed data (e.g. `entity_aliases` for search). |
|
|
187
|
+
|
|
188
|
+
### CRM prospects (4)
|
|
189
|
+
|
|
190
|
+
| Tool | Purpose |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `prospect_create` / `prospect_update` / `prospect_list` / `prospect_search` | Sales pipeline. Stages: `new → contacted → responded → interested → ready_to_buy → proposal_sent → negotiating → closed_won / closed_lost / nurturing`. |
|
|
193
|
+
|
|
194
|
+
### Blog (4)
|
|
195
|
+
|
|
196
|
+
| Tool | Purpose |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `blog_create` / `blog_update` / `blog_list` / `blog_delete` | Blog post content management. |
|
|
199
|
+
|
|
200
|
+
### Email + content queues (3)
|
|
201
|
+
|
|
202
|
+
| Tool | Purpose |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `email_queue_add` | Queue an email (does NOT send — sending is done out-of-band). |
|
|
205
|
+
| `content_queue_add` / `content_queue_list` | Content calendar for scheduled posts. |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Common failures (and how to recover)
|
|
210
|
+
|
|
211
|
+
### "The 'X' table does not exist yet. Run setup_migrate to create the database schema."
|
|
212
|
+
|
|
213
|
+
**What it means:** The collection backing this tool hasn't been created.
|
|
214
|
+
|
|
215
|
+
**Fix:** `setup_migrate` only *reports* missing tables — it does not apply them. You need to run the actual migrations:
|
|
216
|
+
|
|
217
|
+
- **PocketBase:** `./pocketbase migrate up` (after copying the files in `migrations/pocketbase/` into your `pb_migrations/` directory).
|
|
218
|
+
- **Supabase:** run each file in `migrations/supabase/` in order via the SQL editor or `psql`.
|
|
219
|
+
|
|
220
|
+
Then call `setup_status` to confirm.
|
|
221
|
+
|
|
222
|
+
### "Only knowledge tools work, everything else fails"
|
|
223
|
+
|
|
224
|
+
**Symptom:** `knowledge_store` and `knowledge_recall` succeed but `goal_create`, `task_create`, `contact_create` all return the "table does not exist" error.
|
|
225
|
+
|
|
226
|
+
**Cause:** You applied only the first migration (`001_core_schema`) which creates `knowledge`, `decisions`, and `sessions`. The rest of the collections come from migrations `002` through `010` (Supabase) or `002` through `008` (PocketBase).
|
|
227
|
+
|
|
228
|
+
**Fix:** Apply all migrations in order.
|
|
229
|
+
|
|
230
|
+
### PocketBase disconnects between terminal sessions
|
|
231
|
+
|
|
232
|
+
**Cause:** `pocketbase serve` runs in the foreground. When you close the terminal, the server stops.
|
|
233
|
+
|
|
234
|
+
**Fix options:**
|
|
235
|
+
- Run PocketBase under a process manager (pm2, forever) or a launchd plist on macOS.
|
|
236
|
+
- Switch to the Supabase backend — it runs 24/7 in the cloud.
|
|
237
|
+
|
|
238
|
+
### MCP server disconnected after Claude Code restart
|
|
239
|
+
|
|
240
|
+
**Cause:** Your MCP client is not reading the server config on startup, or the `npx -y` download got interrupted.
|
|
241
|
+
|
|
242
|
+
**Fix:** Install globally once (`npm install -g @iwo-szapar/data-mcp`) and point `command` at `data-mcp` instead of `npx`. Restart your MCP client.
|
|
243
|
+
|
|
244
|
+
### "Database authentication failed"
|
|
245
|
+
|
|
246
|
+
**PocketBase:** check `SB_POCKETBASE_ADMIN_EMAIL` / `PASSWORD` match an admin account in the Admin UI.
|
|
247
|
+
|
|
248
|
+
**Supabase:** confirm you are using the `service_role` key, not `anon`. The anon key does not have write access to these tables.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Schema mapping (optional)
|
|
253
|
+
|
|
254
|
+
If your real tables have different names, set `SB_SCHEMA_MAP` to a JSON object:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
SB_SCHEMA_MAP='{"prospects":"sales_leads","contacts":"people"}'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Logical names used by the tools (`prospects`, `contacts`, etc.) are transparently rewritten to your real table names. Empty keys or missing keys pass through unchanged.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## File layout
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
dist/ compiled JS (entry: dist/index.js)
|
|
268
|
+
migrations/
|
|
269
|
+
pocketbase/ *.js migration files (PocketBase migrate format)
|
|
270
|
+
supabase/ *.sql migration files (run in order)
|
|
271
|
+
seed/ seed data (entity_aliases.json, etc.)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The published package ships `dist/`, `migrations/`, `seed/`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Support
|
|
285
|
+
|
|
286
|
+
This package is maintained by [Iwo Szapar](https://iwoszapar.com) as part of the Second Brain ecosystem. For issues specific to Second Brain v2 customers, reply to your purchase confirmation email. For general bugs, open an issue against the package on npm.
|
package/dist/server.js
CHANGED
|
@@ -29,8 +29,8 @@ const SERVER_INSTRUCTIONS = `You are the user's AI Second Brain data layer. This
|
|
|
29
29
|
- Use setup_status to check database readiness`;
|
|
30
30
|
export function createServer(adapter) {
|
|
31
31
|
const server = new McpServer({
|
|
32
|
-
name: '@
|
|
33
|
-
version: '0.
|
|
32
|
+
name: '@iwo-szapar/data-mcp',
|
|
33
|
+
version: '0.3.2',
|
|
34
34
|
}, {
|
|
35
35
|
instructions: SERVER_INSTRUCTIONS,
|
|
36
36
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: link_create
|
|
3
|
+
*
|
|
4
|
+
* Creates a typed relationship between two MemoryOS entities.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '../shared.js';
|
|
8
|
+
const ENTITY_TYPES = ['knowledge', 'decision', 'session', 'blog_post', 'prospect', 'agent_learning'];
|
|
9
|
+
const RELATION_TYPES = ['supports', 'contradicts', 'derived_from', 'example_of', 'supersedes', 'part_of', 'prerequisite'];
|
|
10
|
+
export function registerLinkCreate(server, adapter) {
|
|
11
|
+
server.tool('link_create', 'Create a typed relationship between two MemoryOS entities. ' +
|
|
12
|
+
'Links express how knowledge items relate: supports, contradicts, derived_from, etc. ' +
|
|
13
|
+
'Deduplicates by (source, target, relation_type).', {
|
|
14
|
+
source_type: z.enum(ENTITY_TYPES).describe('Type of the source entity'),
|
|
15
|
+
source_id: z.string().uuid().describe('UUID of the source entity'),
|
|
16
|
+
target_type: z.enum(ENTITY_TYPES).describe('Type of the target entity'),
|
|
17
|
+
target_id: z.string().uuid().describe('UUID of the target entity'),
|
|
18
|
+
relation_type: z.enum(RELATION_TYPES).describe('Type of relationship'),
|
|
19
|
+
confidence: z.number().min(0).max(1).optional().default(0.8).describe('Confidence in this relationship (0-1)'),
|
|
20
|
+
notes: z.string().max(500).optional().describe('Optional context for why this link exists'),
|
|
21
|
+
}, withGracefulDegradation('knowledge_links', adapter, async (params) => {
|
|
22
|
+
try {
|
|
23
|
+
if (params.source_type === params.target_type && params.source_id === params.target_id) {
|
|
24
|
+
return makeToolResponse({
|
|
25
|
+
created: false,
|
|
26
|
+
message: 'Cannot create a self-link.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const existing = await adapter.list('knowledge_links', {
|
|
30
|
+
filter: [[
|
|
31
|
+
{ field: 'source_type', op: 'eq', value: params.source_type },
|
|
32
|
+
{ field: 'source_id', op: 'eq', value: params.source_id },
|
|
33
|
+
{ field: 'target_type', op: 'eq', value: params.target_type },
|
|
34
|
+
{ field: 'target_id', op: 'eq', value: params.target_id },
|
|
35
|
+
{ field: 'relation_type', op: 'eq', value: params.relation_type },
|
|
36
|
+
]],
|
|
37
|
+
page: { limit: 1, offset: 0 },
|
|
38
|
+
});
|
|
39
|
+
if (existing.items.length > 0) {
|
|
40
|
+
return makeToolResponse({
|
|
41
|
+
created: false,
|
|
42
|
+
existing_link_id: existing.items[0].id,
|
|
43
|
+
message: `Link already exists (id: ${existing.items[0].id}).`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const record = await adapter.create('knowledge_links', {
|
|
47
|
+
source_type: params.source_type,
|
|
48
|
+
source_id: params.source_id,
|
|
49
|
+
target_type: params.target_type,
|
|
50
|
+
target_id: params.target_id,
|
|
51
|
+
relation_type: params.relation_type,
|
|
52
|
+
confidence: params.confidence ?? 0.8,
|
|
53
|
+
notes: params.notes ?? null,
|
|
54
|
+
auto_suggested: false,
|
|
55
|
+
});
|
|
56
|
+
return makeToolResponse({
|
|
57
|
+
created: true,
|
|
58
|
+
link: { id: record.id, source_type: params.source_type, target_type: params.target_type, relation_type: params.relation_type },
|
|
59
|
+
message: `Link created: ${params.source_type}:${params.source_id.slice(0, 8)} --[${params.relation_type}]--> ${params.target_type}:${params.target_id.slice(0, 8)}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return handleAdapterError(error, 'link_create');
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=link-create.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: link_delete
|
|
3
|
+
*
|
|
4
|
+
* Deletes a knowledge link by ID.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '../shared.js';
|
|
8
|
+
export function registerLinkDelete(server, adapter) {
|
|
9
|
+
server.tool('link_delete', 'Delete a knowledge link by its ID.', {
|
|
10
|
+
link_id: z.string().uuid().describe('UUID of the link to delete'),
|
|
11
|
+
}, withGracefulDegradation('knowledge_links', adapter, async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
try {
|
|
14
|
+
await adapter.getOne('knowledge_links', params.link_id);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return makeToolResponse({ deleted: false, message: `Link not found: ${params.link_id}` });
|
|
18
|
+
}
|
|
19
|
+
await adapter.delete('knowledge_links', params.link_id);
|
|
20
|
+
return makeToolResponse({ deleted: true, link_id: params.link_id, message: `Link deleted: ${params.link_id}` });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return handleAdapterError(error, 'link_delete');
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=link-delete.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: link_related
|
|
3
|
+
*
|
|
4
|
+
* Get all links for an entity — traverse the knowledge graph.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '../shared.js';
|
|
8
|
+
const ENTITY_TYPES = ['knowledge', 'decision', 'session', 'blog_post', 'prospect', 'agent_learning'];
|
|
9
|
+
export function registerLinkRelated(server, adapter) {
|
|
10
|
+
server.tool('link_related', 'Get all links for an entity. Shows outgoing and incoming relationships with resolved titles.', {
|
|
11
|
+
entity_type: z.enum(ENTITY_TYPES).describe('Type of the entity'),
|
|
12
|
+
entity_id: z.string().uuid().describe('UUID of the entity'),
|
|
13
|
+
direction: z.enum(['both', 'outgoing', 'incoming']).optional().default('both').describe('Filter direction'),
|
|
14
|
+
relation_type: z.enum(['supports', 'contradicts', 'derived_from', 'example_of', 'supersedes', 'part_of', 'prerequisite'])
|
|
15
|
+
.optional().describe('Filter by relation type'),
|
|
16
|
+
}, withGracefulDegradation('knowledge_links', adapter, async (params) => {
|
|
17
|
+
try {
|
|
18
|
+
const outgoing = [];
|
|
19
|
+
const incoming = [];
|
|
20
|
+
if (params.direction === 'both' || params.direction === 'outgoing') {
|
|
21
|
+
const filter = [
|
|
22
|
+
{ field: 'source_type', op: 'eq', value: params.entity_type },
|
|
23
|
+
{ field: 'source_id', op: 'eq', value: params.entity_id },
|
|
24
|
+
];
|
|
25
|
+
if (params.relation_type) {
|
|
26
|
+
filter.push({ field: 'relation_type', op: 'eq', value: params.relation_type });
|
|
27
|
+
}
|
|
28
|
+
const result = await adapter.list('knowledge_links', {
|
|
29
|
+
filter: [filter],
|
|
30
|
+
sort: [{ field: 'created_at', direction: 'desc' }],
|
|
31
|
+
page: { limit: 50, offset: 0 },
|
|
32
|
+
});
|
|
33
|
+
for (const link of result.items) {
|
|
34
|
+
let targetTitle = null;
|
|
35
|
+
try {
|
|
36
|
+
const col = link.target_type === 'decision' ? 'decisions' : link.target_type;
|
|
37
|
+
targetTitle = (await adapter.getOne(col, link.target_id)).title ?? null;
|
|
38
|
+
}
|
|
39
|
+
catch { /* skip */ }
|
|
40
|
+
outgoing.push({
|
|
41
|
+
link_id: link.id, direction: 'outgoing',
|
|
42
|
+
linked_type: link.target_type, linked_id: link.target_id,
|
|
43
|
+
linked_title: targetTitle, relation_type: link.relation_type,
|
|
44
|
+
confidence: link.confidence, notes: link.notes,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (params.direction === 'both' || params.direction === 'incoming') {
|
|
49
|
+
const filter = [
|
|
50
|
+
{ field: 'target_type', op: 'eq', value: params.entity_type },
|
|
51
|
+
{ field: 'target_id', op: 'eq', value: params.entity_id },
|
|
52
|
+
];
|
|
53
|
+
if (params.relation_type) {
|
|
54
|
+
filter.push({ field: 'relation_type', op: 'eq', value: params.relation_type });
|
|
55
|
+
}
|
|
56
|
+
const result = await adapter.list('knowledge_links', {
|
|
57
|
+
filter: [filter],
|
|
58
|
+
sort: [{ field: 'created_at', direction: 'desc' }],
|
|
59
|
+
page: { limit: 50, offset: 0 },
|
|
60
|
+
});
|
|
61
|
+
for (const link of result.items) {
|
|
62
|
+
let sourceTitle = null;
|
|
63
|
+
try {
|
|
64
|
+
const col = link.source_type === 'decision' ? 'decisions' : link.source_type;
|
|
65
|
+
sourceTitle = (await adapter.getOne(col, link.source_id)).title ?? null;
|
|
66
|
+
}
|
|
67
|
+
catch { /* skip */ }
|
|
68
|
+
incoming.push({
|
|
69
|
+
link_id: link.id, direction: 'incoming',
|
|
70
|
+
linked_type: link.source_type, linked_id: link.source_id,
|
|
71
|
+
linked_title: sourceTitle, relation_type: link.relation_type,
|
|
72
|
+
confidence: link.confidence, notes: link.notes,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const allLinks = [...outgoing, ...incoming];
|
|
77
|
+
return makeToolResponse({
|
|
78
|
+
entity_type: params.entity_type, entity_id: params.entity_id,
|
|
79
|
+
total_links: allLinks.length, outgoing_count: outgoing.length, incoming_count: incoming.length,
|
|
80
|
+
links: allLinks,
|
|
81
|
+
message: allLinks.length === 0
|
|
82
|
+
? `No links found. Use link_suggest to find potential connections.`
|
|
83
|
+
: `Found ${allLinks.length} links (${outgoing.length} outgoing, ${incoming.length} incoming).`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return handleAdapterError(error, 'link_related');
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=link-related.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: link_suggest
|
|
3
|
+
*
|
|
4
|
+
* Find similar items and suggest links using keyword matching.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { makeToolResponse, handleAdapterError, withGracefulDegradation } from '../shared.js';
|
|
8
|
+
export function registerLinkSuggest(server, adapter) {
|
|
9
|
+
server.tool('link_suggest', 'Find knowledge items similar to a given item and suggest links. ' +
|
|
10
|
+
'Uses text search to find related items. Returns matches with suggested relation types.', {
|
|
11
|
+
item_id: z.string().uuid().describe('UUID of the knowledge item to find suggestions for'),
|
|
12
|
+
limit: z.number().min(1).max(20).optional().default(5).describe('Max suggestions'),
|
|
13
|
+
}, withGracefulDegradation('knowledge', adapter, async (params) => {
|
|
14
|
+
try {
|
|
15
|
+
let sourceItem;
|
|
16
|
+
try {
|
|
17
|
+
sourceItem = await adapter.getOne('knowledge', params.item_id);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return makeToolResponse({ suggestions: [], message: `Item not found: ${params.item_id}` });
|
|
21
|
+
}
|
|
22
|
+
// Get already-linked IDs to exclude
|
|
23
|
+
const linkedIds = new Set();
|
|
24
|
+
try {
|
|
25
|
+
const out = await adapter.list('knowledge_links', {
|
|
26
|
+
filter: [[{ field: 'source_type', op: 'eq', value: 'knowledge' }, { field: 'source_id', op: 'eq', value: params.item_id }]],
|
|
27
|
+
page: { limit: 100, offset: 0 },
|
|
28
|
+
});
|
|
29
|
+
const inc = await adapter.list('knowledge_links', {
|
|
30
|
+
filter: [[{ field: 'target_type', op: 'eq', value: 'knowledge' }, { field: 'target_id', op: 'eq', value: params.item_id }]],
|
|
31
|
+
page: { limit: 100, offset: 0 },
|
|
32
|
+
});
|
|
33
|
+
for (const l of out.items) linkedIds.add(l.target_id);
|
|
34
|
+
for (const l of inc.items) linkedIds.add(l.source_id);
|
|
35
|
+
}
|
|
36
|
+
catch { /* knowledge_links may not exist */ }
|
|
37
|
+
const searchTerms = extractKeyTerms(sourceItem.title, sourceItem.content);
|
|
38
|
+
let suggestions = [];
|
|
39
|
+
if (searchTerms) {
|
|
40
|
+
const results = await adapter.textSearch('knowledge', searchTerms, {
|
|
41
|
+
limit: (params.limit ?? 5) + linkedIds.size + 1,
|
|
42
|
+
});
|
|
43
|
+
suggestions = results
|
|
44
|
+
.filter(item => item.id !== params.item_id && !linkedIds.has(item.id))
|
|
45
|
+
.slice(0, params.limit ?? 5)
|
|
46
|
+
.map(item => ({
|
|
47
|
+
id: item.id, type: item.type, title: item.title,
|
|
48
|
+
summary: item.summary ?? (item.content?.slice(0, 100) + '...'),
|
|
49
|
+
suggested_relation: suggestRelationType(sourceItem, item),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
return makeToolResponse({
|
|
53
|
+
source: { id: sourceItem.id, type: sourceItem.type, title: sourceItem.title },
|
|
54
|
+
suggestions, already_linked: linkedIds.size,
|
|
55
|
+
message: suggestions.length === 0
|
|
56
|
+
? `No unlinked similar items found for "${sourceItem.title}".`
|
|
57
|
+
: `Found ${suggestions.length} suggestions. Use link_create to connect them.`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return handleAdapterError(error, 'link_suggest');
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function extractKeyTerms(title, content) {
|
|
66
|
+
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
67
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may',
|
|
68
|
+
'might', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
|
|
69
|
+
'and', 'but', 'or', 'not', 'so', 'if', 'when', 'how', 'what', 'which', 'who',
|
|
70
|
+
'this', 'that', 'these', 'those', 'it', 'its']);
|
|
71
|
+
const text = `${title} ${(content ?? '').slice(0, 200)}`;
|
|
72
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/)
|
|
73
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
74
|
+
return [...new Set(words)].slice(0, 5).join(' ');
|
|
75
|
+
}
|
|
76
|
+
function suggestRelationType(source, target) {
|
|
77
|
+
if (source.type === target.type) return 'supports';
|
|
78
|
+
if ((source.type === 'lesson' && target.type === 'decision') || (source.type === 'decision' && target.type === 'lesson')) return 'derived_from';
|
|
79
|
+
if (source.type === 'insight' && target.type === 'pattern') return 'derived_from';
|
|
80
|
+
if (source.type === 'pattern' && target.type === 'insight') return 'example_of';
|
|
81
|
+
return 'supports';
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=link-suggest.js.map
|
package/dist/tools/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool registration — imports and calls all
|
|
2
|
+
* Tool registration — imports and calls all 39 register functions.
|
|
3
3
|
*/
|
|
4
|
-
// Memory tools (
|
|
4
|
+
// Memory tools (26)
|
|
5
5
|
import { registerKnowledgeStore } from './memory/knowledge-store.js';
|
|
6
6
|
import { registerKnowledgeRecall } from './memory/knowledge-recall.js';
|
|
7
7
|
import { registerKnowledgeLearn } from './memory/knowledge-learn.js';
|
|
@@ -24,6 +24,10 @@ import { registerContactList } from './memory/contact-list.js';
|
|
|
24
24
|
import { registerContactSearch } from './memory/contact-search.js';
|
|
25
25
|
import { registerBrainStats } from './memory/brain-stats.js';
|
|
26
26
|
import { registerBrainDecay } from './memory/brain-decay.js';
|
|
27
|
+
import { registerLinkCreate } from './memory/link-create.js';
|
|
28
|
+
import { registerLinkDelete } from './memory/link-delete.js';
|
|
29
|
+
import { registerLinkRelated } from './memory/link-related.js';
|
|
30
|
+
import { registerLinkSuggest } from './memory/link-suggest.js';
|
|
27
31
|
// Setup tools (3)
|
|
28
32
|
import { registerSetupStatus } from './setup/setup-status.js';
|
|
29
33
|
import { registerSetupMigrate } from './setup/setup-migrate.js';
|
|
@@ -64,6 +68,10 @@ export function registerAllTools(server, adapter) {
|
|
|
64
68
|
registerContactSearch(server, adapter);
|
|
65
69
|
registerBrainStats(server, adapter);
|
|
66
70
|
registerBrainDecay(server, adapter);
|
|
71
|
+
registerLinkCreate(server, adapter);
|
|
72
|
+
registerLinkDelete(server, adapter);
|
|
73
|
+
registerLinkRelated(server, adapter);
|
|
74
|
+
registerLinkSuggest(server, adapter);
|
|
67
75
|
// Setup tools
|
|
68
76
|
registerSetupStatus(server, adapter);
|
|
69
77
|
registerSetupMigrate(server, adapter);
|