@iwo-szapar/data-mcp 0.2.1 → 0.3.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.
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { DataAdapter } from '../../adapter/types.js';
3
+ export declare function registerLinkCreate(server: McpServer, adapter: DataAdapter): void;
4
+ //# sourceMappingURL=link-create.d.ts.map
@@ -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,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { DataAdapter } from '../../adapter/types.js';
3
+ export declare function registerLinkDelete(server: McpServer, adapter: DataAdapter): void;
4
+ //# sourceMappingURL=link-delete.d.ts.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,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { DataAdapter } from '../../adapter/types.js';
3
+ export declare function registerLinkRelated(server: McpServer, adapter: DataAdapter): void;
4
+ //# sourceMappingURL=link-related.d.ts.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,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { DataAdapter } from '../../adapter/types.js';
3
+ export declare function registerLinkSuggest(server: McpServer, adapter: DataAdapter): void;
4
+ //# sourceMappingURL=link-suggest.d.ts.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
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Tool registration — imports and calls all 35 register functions.
2
+ * Tool registration — imports and calls all 39 register functions.
3
3
  */
4
- // Memory tools (22)
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);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * PocketBase migration: knowledge_links collection
3
+ *
4
+ * Creates the knowledge_links collection for typed relationships
5
+ * between MemoryOS entities.
6
+ *
7
+ * Note: PocketBase does not support pgvector.
8
+ * Link suggestions use keyword-based text search fallback.
9
+ */
10
+ module.exports = {
11
+ async up(db) {
12
+ const collection = new Collection({
13
+ name: 'knowledge_links',
14
+ type: 'base',
15
+ schema: [
16
+ { name: 'owner_id', type: 'text', required: true, options: { maxSize: 100 } },
17
+ { name: 'source_type', type: 'text', required: true, options: { maxSize: 50 } },
18
+ { name: 'source_id', type: 'text', required: true, options: { maxSize: 36 } },
19
+ { name: 'target_type', type: 'text', required: true, options: { maxSize: 50 } },
20
+ { name: 'target_id', type: 'text', required: true, options: { maxSize: 36 } },
21
+ { name: 'relation_type', type: 'text', required: true, options: { maxSize: 50 } },
22
+ { name: 'confidence', type: 'number', options: { min: 0, max: 1 } },
23
+ { name: 'notes', type: 'text', options: { maxSize: 500 } },
24
+ { name: 'auto_suggested', type: 'bool' },
25
+ ],
26
+ indexes: [
27
+ 'CREATE INDEX idx_kl_source ON knowledge_links (owner_id, source_type, source_id)',
28
+ 'CREATE INDEX idx_kl_target ON knowledge_links (owner_id, target_type, target_id)',
29
+ 'CREATE UNIQUE INDEX idx_kl_unique ON knowledge_links (owner_id, source_type, source_id, target_type, target_id, relation_type)',
30
+ ],
31
+ });
32
+ return db.save(collection);
33
+ },
34
+ async down(db) {
35
+ const collection = await db.findCollectionByNameOrId('knowledge_links');
36
+ return db.delete(collection);
37
+ },
38
+ };
@@ -0,0 +1,47 @@
1
+ -- Migration 010: Knowledge Links + pgvector embeddings
2
+ -- Adds graph-lite relationship system between MemoryOS entities
3
+
4
+ -- 1. Enable pgvector extension
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ -- 2. Add embedding column to knowledge
8
+ ALTER TABLE knowledge ADD COLUMN IF NOT EXISTS embedding vector(384);
9
+
10
+ -- 3. HNSW index for cosine similarity
11
+ CREATE INDEX IF NOT EXISTS idx_knowledge_embedding
12
+ ON knowledge USING hnsw (embedding vector_cosine_ops)
13
+ WITH (m = 16, ef_construction = 64);
14
+
15
+ -- 4. Knowledge links table
16
+ CREATE TABLE IF NOT EXISTS knowledge_links (
17
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ owner_id text NOT NULL DEFAULT 'default',
19
+
20
+ source_type text NOT NULL,
21
+ source_id uuid NOT NULL,
22
+ target_type text NOT NULL,
23
+ target_id uuid NOT NULL,
24
+ relation_type text NOT NULL,
25
+
26
+ confidence numeric DEFAULT 0.8 CHECK (confidence >= 0 AND confidence <= 1),
27
+ notes text,
28
+ auto_suggested boolean DEFAULT false,
29
+ created_at timestamptz DEFAULT now(),
30
+
31
+ CONSTRAINT knowledge_links_source_type_check CHECK (source_type IN ('knowledge', 'decision', 'session', 'blog_post', 'prospect', 'agent_learning')),
32
+ CONSTRAINT knowledge_links_target_type_check CHECK (target_type IN ('knowledge', 'decision', 'session', 'blog_post', 'prospect', 'agent_learning')),
33
+ CONSTRAINT knowledge_links_relation_type_check CHECK (relation_type IN ('supports', 'contradicts', 'derived_from', 'example_of', 'supersedes', 'part_of', 'prerequisite')),
34
+ CONSTRAINT knowledge_links_no_self_link CHECK (NOT (source_type = target_type AND source_id = target_id)),
35
+ CONSTRAINT knowledge_links_unique_link UNIQUE (owner_id, source_type, source_id, target_type, target_id, relation_type)
36
+ );
37
+
38
+ -- 5. Indexes
39
+ CREATE INDEX idx_knowledge_links_source ON knowledge_links (owner_id, source_type, source_id);
40
+ CREATE INDEX idx_knowledge_links_target ON knowledge_links (owner_id, target_type, target_id);
41
+ CREATE INDEX idx_knowledge_links_relation ON knowledge_links (relation_type);
42
+
43
+ -- 6. RLS
44
+ ALTER TABLE knowledge_links ENABLE ROW LEVEL SECURITY;
45
+
46
+ CREATE POLICY "owner_all_access" ON knowledge_links
47
+ FOR ALL USING (owner_id = current_setting('app.owner_id', true) OR current_setting('app.owner_id', true) IS NULL);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iwo-szapar/data-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Unified data MCP server for Second Brain \u2014 PocketBase and Supabase adapters",
5
5
  "type": "module",
6
6
  "bin": {