cordenar-mcp 0.1.1

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/db.js ADDED
@@ -0,0 +1,95 @@
1
+ // Cordenar MCP — Sync database (sync.db)
2
+ // Stores the synapse_map: cloud synapse UUID ↔ local entity ID
3
+ import Database from 'better-sqlite3';
4
+ import { getDbPath } from './config.js';
5
+
6
+ let db;
7
+
8
+ export function initDb() {
9
+ if (db) return db;
10
+ const path = getDbPath();
11
+ db = new Database(path);
12
+ db.pragma('journal_mode = WAL');
13
+ db.pragma('busy_timeout = 5000');
14
+
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS synapse_map (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ cloud_id TEXT NOT NULL UNIQUE,
19
+ source TEXT NOT NULL,
20
+ local_id INTEGER NOT NULL,
21
+ local_hash TEXT NOT NULL,
22
+ cloud_hash TEXT NOT NULL,
23
+ direction TEXT NOT NULL DEFAULT 'push',
24
+ synced_at INTEGER NOT NULL DEFAULT (unixepoch())
25
+ );
26
+ CREATE INDEX IF NOT EXISTS idx_synapse_map_lookup
27
+ ON synapse_map(source, local_id);
28
+ CREATE INDEX IF NOT EXISTS idx_synapse_map_cloud
29
+ ON synapse_map(cloud_id);
30
+ `);
31
+
32
+ return db;
33
+ }
34
+
35
+ export function lookupLocal(source, localId) {
36
+ initDb();
37
+ return db
38
+ .prepare('SELECT * FROM synapse_map WHERE source = ? AND local_id = ?')
39
+ .get(source, localId);
40
+ }
41
+
42
+ export function lookupCloud(cloudId) {
43
+ initDb();
44
+ return db
45
+ .prepare('SELECT * FROM synapse_map WHERE cloud_id = ?')
46
+ .get(cloudId);
47
+ }
48
+
49
+ export function storeMapping({
50
+ cloud_id,
51
+ source,
52
+ local_id,
53
+ local_hash,
54
+ cloud_hash,
55
+ direction,
56
+ }) {
57
+ initDb();
58
+ db.prepare(
59
+ `INSERT INTO synapse_map
60
+ (cloud_id, source, local_id, local_hash, cloud_hash, direction, synced_at)
61
+ VALUES (?, ?, ?, ?, ?, ?, unixepoch())
62
+ ON CONFLICT(cloud_id) DO UPDATE SET
63
+ local_hash = excluded.local_hash,
64
+ cloud_hash = excluded.cloud_hash,
65
+ synced_at = unixepoch()`
66
+ ).run(cloud_id, source, local_id, local_hash, cloud_hash, direction);
67
+ }
68
+
69
+ export function removeMapping(cloudId) {
70
+ initDb();
71
+ db.prepare('DELETE FROM synapse_map WHERE cloud_id = ?').run(cloudId);
72
+ }
73
+
74
+ export function getPushEntries() {
75
+ initDb();
76
+ return db
77
+ .prepare("SELECT * FROM synapse_map WHERE direction = 'push'")
78
+ .all();
79
+ }
80
+
81
+ export function getPullEntries() {
82
+ initDb();
83
+ return db
84
+ .prepare("SELECT * FROM synapse_map WHERE direction = 'pull'")
85
+ .all();
86
+ }
87
+
88
+ export function getDirtyEntries() {
89
+ initDb();
90
+ return db
91
+ .prepare(
92
+ "SELECT * FROM synapse_map WHERE direction = 'push' AND local_hash != cloud_hash"
93
+ )
94
+ .all();
95
+ }
package/embedding.js ADDED
@@ -0,0 +1,84 @@
1
+ // Cordenar MCP — MurmurHash3 256-dim LSH embedding
2
+ // Deterministic, no external API, ~2µs per call
3
+ // Identical implementation to Hemisphere and Compend
4
+ const DIM = 256;
5
+
6
+ function murmurHash3(key, seed = 0) {
7
+ let h1 = seed >>> 0;
8
+ const remainder = key.length & 3;
9
+ const bytes = key.length - remainder;
10
+ const c1 = 0xcc9e2d51;
11
+ const c2 = 0x1b873593;
12
+
13
+ for (let i = 0; i < bytes; i += 4) {
14
+ let k1 = (key.charCodeAt(i) & 0xff)
15
+ | ((key.charCodeAt(i + 1) & 0xff) << 8)
16
+ | ((key.charCodeAt(i + 2) & 0xff) << 16)
17
+ | ((key.charCodeAt(i + 3) & 0xff) << 24);
18
+
19
+ k1 = Math.imul(k1, c1);
20
+ k1 = (k1 << 15) | (k1 >>> 17);
21
+ k1 = Math.imul(k1, c2);
22
+
23
+ h1 ^= k1;
24
+ h1 = (h1 << 13) | (h1 >>> 19);
25
+ h1 = Math.imul(h1, 5) + 0xe6546b64;
26
+ }
27
+
28
+ if (remainder > 0) {
29
+ let k1 = 0;
30
+ for (let j = remainder - 1; j >= 0; j--) {
31
+ k1 = (k1 << 8) | (key.charCodeAt(bytes + j) & 0xff);
32
+ }
33
+ k1 = Math.imul(k1, c1);
34
+ k1 = (k1 << 15) | (k1 >>> 17);
35
+ k1 = Math.imul(k1, c2);
36
+ h1 ^= k1;
37
+ }
38
+
39
+ h1 ^= key.length;
40
+ h1 ^= h1 >>> 16;
41
+ h1 = Math.imul(h1, 0x85ebca6b);
42
+ h1 ^= h1 >>> 13;
43
+ h1 = Math.imul(h1, 0xc2b2ae35);
44
+ h1 ^= h1 >>> 16;
45
+
46
+ return h1 >>> 0;
47
+ }
48
+
49
+ function hashVector(str) {
50
+ const hash = murmurHash3(str);
51
+ const idx = hash % DIM;
52
+ const sign = (hash & 1) ? 1 : -1;
53
+ return { idx, sign };
54
+ }
55
+
56
+ export function createEmbedding(text) {
57
+ const vector = new Float32Array(DIM);
58
+
59
+ const tokens = text.toLowerCase().split(/[^\w']+/).filter(t => t.length > 0);
60
+
61
+ for (const token of tokens) {
62
+ const { idx, sign } = hashVector(token);
63
+ vector[idx] += sign;
64
+ }
65
+
66
+ for (let i = 0; i < tokens.length - 1; i++) {
67
+ const bigram = tokens[i] + '\x00' + tokens[i + 1];
68
+ const { idx, sign } = hashVector(bigram);
69
+ vector[idx] += sign;
70
+ }
71
+
72
+ let sumSq = 0;
73
+ for (let i = 0; i < DIM; i++) {
74
+ sumSq += vector[i] * vector[i];
75
+ }
76
+ const norm = Math.sqrt(sumSq);
77
+ if (norm > 0) {
78
+ for (let i = 0; i < DIM; i++) {
79
+ vector[i] /= norm;
80
+ }
81
+ }
82
+
83
+ return vector;
84
+ }
package/index.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+ import http from 'node:http';
9
+ import { initDb } from './db.js';
10
+ import { getConfig } from './config.js';
11
+ import { listSynapses, shareSynapses, pullSynapses, fullSync } from './sync.js';
12
+
13
+ const DASH_PORT = getConfig().port;
14
+
15
+ const db = initDb();
16
+
17
+ function notifyDash(event, data) {
18
+ const body = JSON.stringify({ event, ...data });
19
+ const req = http.request(
20
+ `http://127.0.0.1:${DASH_PORT}/api/notify`,
21
+ {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'Content-Length': Buffer.byteLength(body),
26
+ },
27
+ }
28
+ );
29
+ req.on('error', () => {});
30
+ req.write(body);
31
+ req.end();
32
+ }
33
+
34
+ process.on('SIGTERM', () => {
35
+ try { db.close(); } catch {}
36
+ process.exit(0);
37
+ });
38
+ process.on('SIGINT', () => {
39
+ try { db.close(); } catch {}
40
+ process.exit(0);
41
+ });
42
+
43
+ const server = new Server(
44
+ { name: 'cordenar-mcp', version: '0.1.0' },
45
+ { capabilities: { tools: {} } }
46
+ );
47
+
48
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
49
+ tools: [
50
+ {
51
+ name: 'cordenar_auth',
52
+ description:
53
+ 'Authenticate this node with Cordenar cloud via device code OAuth flow. Opens a browser or prints a URL + code for the user to enter in the dashboard.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {},
57
+ required: [],
58
+ },
59
+ },
60
+ {
61
+ name: 'cordenar_status',
62
+ description:
63
+ 'Show current auth state, node info, pending push count, un-pulled synapse count, and last sync time.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {},
67
+ required: [],
68
+ },
69
+ },
70
+ {
71
+ name: 'cordenar_list',
72
+ description:
73
+ 'List local synapses (Hemisphere memories + Compend concepts) available for sharing. Supports filters for source, type, and project.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ source: {
78
+ type: 'string',
79
+ description: "Filter by source: 'hemisphere' or 'compend'",
80
+ },
81
+ type: {
82
+ type: 'string',
83
+ description: 'Filter by type: memory kind or concept type',
84
+ },
85
+ project: {
86
+ type: 'string',
87
+ description: 'Filter Hemisphere memories by project namespace',
88
+ },
89
+ limit: {
90
+ type: 'number',
91
+ description: 'Max results (default 50)',
92
+ },
93
+ },
94
+ required: [],
95
+ },
96
+ },
97
+ {
98
+ name: 'cordenar_share',
99
+ description:
100
+ 'Push selected local synapses to the cloud. Synapses enter pending_approval status until an admin approves them.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ synapses: {
105
+ type: 'array',
106
+ description:
107
+ "Array of { source: 'hemisphere'|'compend', id: number } objects to share",
108
+ items: {
109
+ type: 'object',
110
+ properties: {
111
+ source: { type: 'string' },
112
+ id: { type: 'number' },
113
+ },
114
+ required: ['source', 'id'],
115
+ },
116
+ },
117
+ },
118
+ required: ['synapses'],
119
+ },
120
+ },
121
+ {
122
+ name: 'cordenar_pull',
123
+ description:
124
+ 'Fetch approved active synapses from your groups and store them locally.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ group_id: {
129
+ type: 'string',
130
+ description: 'Optional. Limit pull to a specific group.',
131
+ },
132
+ },
133
+ required: [],
134
+ },
135
+ },
136
+ {
137
+ name: 'cordenar_sync',
138
+ description:
139
+ 'Full bidirectional sync: push locally-edited shared synapses, detect and unlink locally-deleted shared synapses, pull new approved synapses from cloud.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {},
143
+ required: [],
144
+ },
145
+ },
146
+ ],
147
+ }));
148
+
149
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
150
+ const { name, arguments: args } = request.params;
151
+ try {
152
+ let result;
153
+ switch (name) {
154
+ case 'cordenar_auth':
155
+ result = { message: 'Not yet implemented' };
156
+ break;
157
+ case 'cordenar_status':
158
+ result = { message: 'Not yet implemented' };
159
+ break;
160
+ case 'cordenar_list':
161
+ result = listSynapses(args);
162
+ break;
163
+ case 'cordenar_share':
164
+ result = shareSynapses(args.synapses);
165
+ notifyDash('synapse_shared', { count: args.synapses?.length || 0 });
166
+ break;
167
+ case 'cordenar_pull':
168
+ result = pullSynapses(args.group_id);
169
+ notifyDash('synapse_pulled', {});
170
+ break;
171
+ case 'cordenar_sync':
172
+ result = fullSync();
173
+ if (result.pushed || result.unlinked || result.pulled) {
174
+ notifyDash('sync_complete', {});
175
+ }
176
+ break;
177
+ default:
178
+ throw new Error(`Unknown tool: ${name}`);
179
+ }
180
+ return {
181
+ content: [
182
+ { type: 'text', text: JSON.stringify(result, null, 2) },
183
+ ],
184
+ };
185
+ } catch (err) {
186
+ const msg =
187
+ err.message && err.message.includes('/')
188
+ ? 'Internal error'
189
+ : err.message;
190
+ return {
191
+ content: [{ type: 'text', text: `Error: ${msg}` }],
192
+ isError: true,
193
+ };
194
+ }
195
+ });
196
+
197
+ const transport = new StdioServerTransport();
198
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "cordenar-mcp",
3
+ "version": "0.1.1",
4
+ "description": "Cloud orchestration MCP server — bridge local Hemisphere/Compend knowledge bases to distributed team synapses",
5
+ "author": "Hector Jarquin",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "main": "index.js",
9
+ "bin": {
10
+ "cordenar": "dashboard.js"
11
+ },
12
+ "scripts": {
13
+ "start": "node dashboard.js",
14
+ "stop": "node dashboard.js stop",
15
+ "restart": "node dashboard.js restart"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/hectorjarquin/cordenar-mcp.git"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.29.0",
23
+ "@supabase/supabase-js": "^2.0.0",
24
+ "better-sqlite3": "^11.0.0"
25
+ }
26
+ }
package/supabase.js ADDED
@@ -0,0 +1,32 @@
1
+ // Cordenar MCP — Supabase client wrapper
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import { getConfig } from './config.js';
4
+ import { getAccessToken } from './auth.js';
5
+
6
+ let _client = null;
7
+
8
+ export function getSupabase() {
9
+ if (_client) return _client;
10
+ const cfg = getConfig();
11
+ if (!cfg.supabaseUrl || !cfg.supabaseAnonKey) {
12
+ throw new Error(
13
+ 'Supabase not configured. Set SUPABASE_URL and SUPABASE_ANON_KEY env vars.'
14
+ );
15
+ }
16
+ _client = createClient(cfg.supabaseUrl, cfg.supabaseAnonKey);
17
+ return _client;
18
+ }
19
+
20
+ export function getAuthenticatedClient() {
21
+ const token = getAccessToken();
22
+ if (!token) {
23
+ throw new Error(
24
+ 'Not authenticated. Run cordenar_auth first.'
25
+ );
26
+ }
27
+ // Create a fresh client with the user's JWT for RLS-scoped queries
28
+ const cfg = getConfig();
29
+ return createClient(cfg.supabaseUrl, cfg.supabaseAnonKey, {
30
+ global: { headers: { Authorization: `Bearer ${token}` } },
31
+ });
32
+ }
package/sync.js ADDED
@@ -0,0 +1,117 @@
1
+ // Cordenar MCP — Sync logic
2
+ // Push, pull, hash-diff, unlink, and SSE revocation
3
+ import { createHash } from 'node:crypto';
4
+ import Database from 'better-sqlite3';
5
+ import { initDb, lookupLocal, lookupCloud, storeMapping, removeMapping, getPushEntries, getDirtyEntries } from './db.js';
6
+ import { getConfig } from './config.js';
7
+
8
+ export function hashContent(content) {
9
+ return createHash('sha256').update(content).digest('hex');
10
+ }
11
+
12
+ // ── cordenar_list ──────────────────────────────────────────────
13
+
14
+ export function listSynapses({ source, type, project, limit = 50 } = {}) {
15
+ const results = [];
16
+ let db;
17
+
18
+ if (!source || source === 'hemisphere') {
19
+ db = openHemisphereReadOnly();
20
+ const memories = db
21
+ .prepare(
22
+ `SELECT id, project, kind, status, substr(content, 1, 200) as preview, created_at
23
+ FROM memories
24
+ WHERE deleted_at IS NULL AND archived_at IS NULL
25
+ ${project ? 'AND project = ?' : ''}
26
+ ${type ? 'AND kind = ?' : ''}
27
+ ORDER BY created_at DESC LIMIT ?`
28
+ )
29
+ .all(
30
+ ...[project, type, limit].filter((v) => v !== undefined && v !== null && v !== '')
31
+ );
32
+ for (const m of memories) {
33
+ results.push({
34
+ source: 'hemisphere',
35
+ id: m.id,
36
+ type: m.kind,
37
+ project: m.project,
38
+ status: m.status,
39
+ preview: m.preview,
40
+ created_at: m.created_at,
41
+ already_shared: !!lookupLocal('hemisphere', m.id),
42
+ });
43
+ }
44
+ db.close();
45
+ }
46
+
47
+ if (!source || source === 'compend') {
48
+ db = openCompendReadOnly();
49
+ const concepts = db
50
+ .prepare(
51
+ `SELECT id, slug, type, title, status, description,
52
+ substr(body, 1, 200) as preview, body, created_at
53
+ FROM concepts
54
+ WHERE source = 'local'
55
+ ${type ? 'AND type = ?' : ''}
56
+ ORDER BY created_at DESC LIMIT ?`
57
+ )
58
+ .all(...[type, limit].filter((v) => v !== undefined && v !== null && v !== ''));
59
+ for (const c of concepts) {
60
+ results.push({
61
+ source: 'compend',
62
+ id: c.id,
63
+ type: c.type,
64
+ slug: c.slug,
65
+ title: c.title,
66
+ description: c.description,
67
+ preview: c.preview,
68
+ body: c.body,
69
+ status: c.status,
70
+ created_at: c.created_at,
71
+ already_shared: !!lookupLocal('compend', c.id),
72
+ });
73
+ }
74
+ db.close();
75
+ }
76
+
77
+ return results.sort((a, b) => b.created_at - a.created_at);
78
+ }
79
+
80
+ // ── cordenar_share ─────────────────────────────────────────────
81
+
82
+ export function shareSynapses(synapses) {
83
+ const shared = [];
84
+ for (const { source, id } of synapses) {
85
+ const existing = lookupLocal(source, id);
86
+ if (existing) {
87
+ shared.push({ source, id, status: 'already_shared', cloud_id: existing.cloud_id });
88
+ continue;
89
+ }
90
+ shared.push({ source, id, status: 'not_implemented' });
91
+ }
92
+ return { shared };
93
+ }
94
+
95
+ // ── cordenar_pull ──────────────────────────────────────────────
96
+
97
+ export function pullSynapses(groupId) {
98
+ return { pulled: 0, message: 'Not yet implemented' };
99
+ }
100
+
101
+ // ── cordenar_sync ──────────────────────────────────────────────
102
+
103
+ export function fullSync() {
104
+ return { pushed: 0, unlinked: 0, pulled: 0, message: 'Not yet implemented' };
105
+ }
106
+
107
+ // ── Local DB read-only helpers ─────────────────────────────────
108
+
109
+ function openHemisphereReadOnly() {
110
+ const cfg = getConfig();
111
+ return new Database(cfg.hemisphereDb, { readonly: true });
112
+ }
113
+
114
+ function openCompendReadOnly() {
115
+ const cfg = getConfig();
116
+ return new Database(cfg.compendDb, { readonly: true });
117
+ }