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/CHANGELOG.md +58 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/auth.js +48 -0
- package/config.js +87 -0
- package/dashboard/api-handler.js +173 -0
- package/dashboard/public/app.js +447 -0
- package/dashboard/public/index.html +96 -0
- package/dashboard/public/logo.svg +1 -0
- package/dashboard/public/style.css +273 -0
- package/dashboard.js +230 -0
- package/db.js +95 -0
- package/embedding.js +84 -0
- package/index.js +198 -0
- package/package.json +26 -0
- package/supabase.js +32 -0
- package/sync.js +117 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.1] - 2026-07-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Compend synapse parity — description, body, and preview now read from
|
|
12
|
+
Compend's concepts table. Content cell fallback chain shows preview,
|
|
13
|
+
title, description, then slug. Detail panel renders full body text.
|
|
14
|
+
Search includes description and body fields.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Page title simplified to "Cordenar".
|
|
18
|
+
- Layout width matched to Compend/Hemisphere — `#app` wrapper at 960px
|
|
19
|
+
content area (was 920px due to padding inside body container).
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-07-10
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Initial scaffold: MCP stdio server with 6 tool registrations (`cordenar_auth`, `cordenar_status`, `cordenar_list`, `cordenar_share`, `cordenar_pull`, `cordenar_sync`)
|
|
26
|
+
- Config singleton (`config.js`) — code defaults, `~/.cordenar/config.json`, environment variable overrides
|
|
27
|
+
- Sync database (`db.js`) — `synapse_map` table with CRUD operations for cloud-to-local ID reconciliation
|
|
28
|
+
- Sync logic (`sync.js`) — `cordenar_list` reads both Hemisphere and Compend databases (read-only), unified synapse view with `already_shared` flag
|
|
29
|
+
- Auth module (`auth.js`) — JWT token store at `~/.cordenar/auth.json`, with load, save, clear, and validation helpers
|
|
30
|
+
- Supabase client wrapper (`supabase.js`) — authenticated and anonymous Supabase client creation
|
|
31
|
+
- Embedding module (`embedding.js`) — MurmurHash3 256-dim LSH vector (copy from Hemisphere, deterministic, ~2µs per call)
|
|
32
|
+
- Local dashboard (port 3458) — vanilla JS SPA with dark/light theme, SSE real-time updates, skeleton loading, toast notifications, WCAG 2.1 AA accessibility
|
|
33
|
+
- Dashboard kebab menu per synapse — context-sensitive actions by status (Share, Unlink, Remove Local, Dismiss)
|
|
34
|
+
- Source and status filters with debounced search
|
|
35
|
+
- Confirm dialog for destructive actions
|
|
36
|
+
- Theme toggle with persistence (`localStorage`) and FOUC prevention via synchronous head script
|
|
37
|
+
- CLI commands (`cordenar`, `cordenar stop`, `cordenar restart`)
|
|
38
|
+
- Global bin entry and npm scripts
|
|
39
|
+
- Project-level OpenCode config with ponytail plugin
|
|
40
|
+
- `.gitignore` and `.npmignore` for database and environment files
|
|
41
|
+
- Real-time SSE — `notifyDash()` wired in MCP index.js with event notifications after share/pull/sync tool calls
|
|
42
|
+
- External event remapping in `/api/notify` handler — `memory_*`/`compend_*`/`concept_*`/`project_*` events remapped to `synapse_updated`
|
|
43
|
+
- Named Service Discovery — `~/.cordenar/manifest.json` written on startup, deleted on shutdown. Producers (Hemisphere/Compend) reference `"notifySubscribers": ["cordenar"]` instead of raw URLs
|
|
44
|
+
- Dashboard search filter in `/api/synapses` (matches preview, title, type)
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Replaced browser fallback polling with `synapse_updated` SSE listener
|
|
49
|
+
- Dynamic imports replaced with static imports in MCP index.js
|
|
50
|
+
- Combined Hemisphere+Compend synapse results sorted by `created_at` DESC
|
|
51
|
+
- `sync_complete` SSE notification gated on actual sync results
|
|
52
|
+
- Event name remapping: external tool events normalized to `synapse_updated` for dashboard consumption
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Dashboard SSE not reaching browser — events now delivered via active HTTP request cycles (`notifyDash` → `/api/notify` → `broadcast`)
|
|
57
|
+
- Dashboard search box returning unfiltered results
|
|
58
|
+
- Removed unused `client` variable in `supabase.js`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hector Jarquin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<img src="dashboard/public/logo.svg" height="48" alt="" />
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT) [](https://nodejs.org)
|
|
4
|
+
|
|
5
|
+
# Cordenar MCP
|
|
6
|
+
|
|
7
|
+
Cloud orchestration MCP server — bridges local [Hemisphere](https://github.com/hectorjarquin/hemisphere) and [Compend](https://github.com/hectorjarquin/compend) knowledge bases to distributed team synapses.
|
|
8
|
+
|
|
9
|
+
Cordenar gives teams the ability to securely share, approve, and synchronize knowledge across MCP nodes. Think of it as the control plane that makes context portable, auditable, and collaborative — whether you're running a single server or a distributed mesh.
|
|
10
|
+
|
|
11
|
+
Built entirely in Node.js with zero cloud dependencies in the MCP layer. Syncing happens through Supabase on the cloud side.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g cordenar-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Once installed, run the dashboard:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cordenar
|
|
23
|
+
# → http://localhost:3458
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### CLI commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cordenar # Start the dashboard
|
|
30
|
+
cordenar stop # Stop a running instance
|
|
31
|
+
cordenar restart # Stop then restart
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### MCP server
|
|
35
|
+
|
|
36
|
+
Add to `opencode.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcp": {
|
|
41
|
+
"cordenar": {
|
|
42
|
+
"type": "local",
|
|
43
|
+
"command": ["node", "/home/user/.nvm/versions/node/v22.22.0/lib/node_modules/cordenar-mcp/index.js"],
|
|
44
|
+
"enabled": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Find your exact path with `npm root -g` — append `/cordenar-mcp/index.js`.
|
|
51
|
+
|
|
52
|
+
### Dashboard
|
|
53
|
+
|
|
54
|
+
Browse and manage your local synapses at `http://localhost:3458`.
|
|
55
|
+
|
|
56
|
+
Port configurable via `~/.cordenar/config.json` or `CORDENAR_PORT`.
|
|
57
|
+
|
|
58
|
+
**Features:** dark/light theme toggle with persistence, WCAG 2.1 AA accessibility (keyboard navigation, ARIA labels, focus-visible outlines), real-time SSE updates (no polling), toast notifications, skeleton loading, search with debounce, source and status filters, expandable detail rows, kebab menu per synapse with context-sensitive actions (Share, Unlink, Remove Local, Dismiss).
|
|
59
|
+
|
|
60
|
+
## How It Works
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Local DBs (Hemisphere/Compend) → Cordenar MCP → Supabase (cloud) → Team nodes
|
|
64
|
+
(read-only) (sync) (orgs/groups) (pull + SSE revoke)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Synapse Lifecycle
|
|
68
|
+
|
|
69
|
+
1. **Share** — Select local synapses from Hemisphere or Compend. Push to cloud as `pending_approval`.
|
|
70
|
+
2. **Approve** — Org admin reviews and approves in the cloud dashboard.
|
|
71
|
+
3. **Sync** — Team members pull approved synapses into their local knowledge bases.
|
|
72
|
+
4. **Revoke** — Admin can revoke access at any time. Synapse is removed from local DBs via SSE.
|
|
73
|
+
|
|
74
|
+
### Architecture
|
|
75
|
+
|
|
76
|
+
Cordenar MCP is **standalone** — it reads Hemisphere and Compend databases directly (read-only) and does not wrap or extend either MCP. Hemisphere and Compend remain sovereign, local-first, open-source MCPs. Cordenar is the cloud bridge for distributed synapse sharing across nodes.
|
|
77
|
+
|
|
78
|
+
### Real-Time Updates
|
|
79
|
+
|
|
80
|
+
Cordenar receives real-time notifications from Hemisphere and Compend via **Named Service Discovery**:
|
|
81
|
+
|
|
82
|
+
1. When Cordenar starts, it writes `~/.cordenar/manifest.json`:
|
|
83
|
+
```json
|
|
84
|
+
{ "name": "cordenar", "notifyEndpoint": "http://127.0.0.1:3458/api/notify" }
|
|
85
|
+
```
|
|
86
|
+
2. Configure Hemisphere and Compend to subscribe by name:
|
|
87
|
+
```json
|
|
88
|
+
// ~/.hemisphere/config.json
|
|
89
|
+
{ "notifySubscribers": ["cordenar"] }
|
|
90
|
+
|
|
91
|
+
// ~/.compend/config.json
|
|
92
|
+
{ "notifySubscribers": ["cordenar"] }
|
|
93
|
+
```
|
|
94
|
+
3. On every tool call (`memory_store`, `compend_index`, etc.), Hemisphere and Compend read the manifest and POST the event to Cordenar's notify endpoint
|
|
95
|
+
4. Cordenar remaps external events (`memory_*`, `compend_*`, `concept_*`, `project_*`) to `synapse_updated` and broadcasts to the browser via SSE
|
|
96
|
+
5. On shutdown, the manifest is deleted. No polling — everything is push-based and real-time.
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
### Prerequisites
|
|
101
|
+
|
|
102
|
+
- Node.js 18+
|
|
103
|
+
- C++ build tools (`build-essential` on Debian/Ubuntu, Xcode CLI tools on macOS) — required to compile `better-sqlite3`
|
|
104
|
+
|
|
105
|
+
### From npm (recommended)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install -g cordenar-mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `cordenar` CLI is now available globally. The MCP server runs via your AI client — see [Configuration](#configuration-1).
|
|
112
|
+
|
|
113
|
+
### From git
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/hectorjarquin/cordenar-mcp.git ~/cordenar
|
|
117
|
+
cd ~/cordenar
|
|
118
|
+
npm install
|
|
119
|
+
npm link # creates global `cordenar` command
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Configuration
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcp": {
|
|
127
|
+
"cordenar": {
|
|
128
|
+
"type": "local",
|
|
129
|
+
"command": ["node", "/usr/lib/node_modules/cordenar-mcp/index.js"],
|
|
130
|
+
"enabled": true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Find your exact path with `npm root -g` — append `/cordenar-mcp/index.js`. For nvm users the path is typically `~/.nvm/versions/node/vX/lib/node_modules/cordenar-mcp/index.js`.
|
|
137
|
+
|
|
138
|
+
## MCP Tools (Human-Facing)
|
|
139
|
+
|
|
140
|
+
These tools are available through your AI client when Cordenar MCP is configured. They provide manual control over synapse sharing — no automatic uploads or background sync.
|
|
141
|
+
|
|
142
|
+
### `cordenar_auth`
|
|
143
|
+
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
No parameters. Returns authentication state and node ID.
|
|
147
|
+
|
|
148
|
+
### `cordenar_status`
|
|
149
|
+
|
|
150
|
+
Show current auth state, node info, pending push count, un-pulled synapse count, and last sync time.
|
|
151
|
+
|
|
152
|
+
No parameters.
|
|
153
|
+
|
|
154
|
+
### `cordenar_list`
|
|
155
|
+
|
|
156
|
+
List local synapses (Hemisphere memories + Compend concepts) available for sharing. Supports filters.
|
|
157
|
+
|
|
158
|
+
| Parameter | Type | Required | Default | Description |
|
|
159
|
+
|-----------|------|----------|---------|-------------|
|
|
160
|
+
| `source` | string | no | — | Filter by source: `'hemisphere'` or `'compend'` |
|
|
161
|
+
| `type` | string | no | — | Filter by type: memory kind or concept type |
|
|
162
|
+
| `project` | string | no | — | Filter Hemisphere memories by project namespace |
|
|
163
|
+
| `limit` | number | no | `50` | Max results |
|
|
164
|
+
|
|
165
|
+
Returns a unified view with `source`, `type`, `preview`, `status`, and `already_shared` flags.
|
|
166
|
+
|
|
167
|
+
### `cordenar_share`
|
|
168
|
+
|
|
169
|
+
Push selected local synapses to the cloud. Synapses enter `pending_approval` status until an admin approves them.
|
|
170
|
+
|
|
171
|
+
| Parameter | Type | Required | Default | Description |
|
|
172
|
+
|-----------|------|----------|---------|-------------|
|
|
173
|
+
| `synapses` | array | yes | — | Array of `{ source, id }` objects to share |
|
|
174
|
+
|
|
175
|
+
### `cordenar_pull`
|
|
176
|
+
|
|
177
|
+
Fetch approved active synapses from your groups and store them locally.
|
|
178
|
+
|
|
179
|
+
| Parameter | Type | Required | Default | Description |
|
|
180
|
+
|-----------|------|----------|---------|-------------|
|
|
181
|
+
| `group_id` | string | no | — | Limit pull to a specific group |
|
|
182
|
+
|
|
183
|
+
Pulled memories are stored in Hemisphere with `project='cordenar'`, `kind='synapse'`. Pulled concepts are stored in Compend with `source='remote'`.
|
|
184
|
+
|
|
185
|
+
### `cordenar_sync`
|
|
186
|
+
|
|
187
|
+
Full bidirectional sync: push locally-edited shared synapses, detect and unlink locally-deleted shared synapses, pull new approved synapses from cloud.
|
|
188
|
+
|
|
189
|
+
No parameters. Returns summary of pushed, unlinked, and pulled counts.
|
|
190
|
+
|
|
191
|
+
## HTTP API (Dashboard-Facing)
|
|
192
|
+
|
|
193
|
+
The dashboard exposes REST endpoints:
|
|
194
|
+
|
|
195
|
+
| Method | Path | Description |
|
|
196
|
+
|--------|------|-------------|
|
|
197
|
+
| `GET` | `/api/synapses` | List synapses with source, status, and search filters |
|
|
198
|
+
| `GET` | `/api/status` | Auth state, push/pull/dirty counts |
|
|
199
|
+
| `GET` | `/api/events` | SSE stream for real-time dashboard updates |
|
|
200
|
+
| `POST` | `/api/sync` | Trigger full bidirectional sync |
|
|
201
|
+
| `POST` | `/api/notify` | SSE event relay. Accepts external events from Hemisphere/Compend (via `notifySubscribers`) and MCP tools (via `notifyDash`). Remaps `memory_*`/`compend_*`/`concept_*`/`project_*` → `synapse_updated`. |
|
|
202
|
+
| `POST` | `/api/synapses/:id/share?source=` | Share a synapse to cloud |
|
|
203
|
+
| `POST` | `/api/synapses/:id/unshare?source=` | Cancel a pending share |
|
|
204
|
+
| `POST` | `/api/synapses/:id/unlink?source=` | Remove mapping, keep local entity |
|
|
205
|
+
| `POST` | `/api/synapses/:id/pull-delete?source=` | Delete local copy and unlink |
|
|
206
|
+
| `POST` | `/api/synapses/:id/dismiss?source=` | Dismiss a rejected synapse |
|
|
207
|
+
|
|
208
|
+
## Configuration
|
|
209
|
+
|
|
210
|
+
### Config File (`~/.cordenar/config.json`)
|
|
211
|
+
|
|
212
|
+
Create an optional JSON config file to customize settings. All keys are optional — missing keys use code defaults.
|
|
213
|
+
|
|
214
|
+
**Priority chain** (highest wins): code defaults < config file < environment variables.
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"port": 3458,
|
|
219
|
+
"dbPath": "~/.cordenar/sync.db",
|
|
220
|
+
"hemisphereDb": "~/.hemisphere/memories.db",
|
|
221
|
+
"compendDb": "~/.compend/concepts.db",
|
|
222
|
+
"supabaseUrl": "",
|
|
223
|
+
"supabaseAnonKey": ""
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Key | Type | Default | Description |
|
|
228
|
+
|-----|------|---------|-------------|
|
|
229
|
+
| `port` | number | `3458` | Dashboard HTTP server port |
|
|
230
|
+
| `dbPath` | string | `~/.cordenar/sync.db` | Sync database path (supports `~`) |
|
|
231
|
+
| `hemisphereDb` | string | `~/.hemisphere/memories.db` | Hemisphere database path |
|
|
232
|
+
| `compendDb` | string | `~/.compend/concepts.db` | Compend database path |
|
|
233
|
+
| `supabaseUrl` | string | — | Supabase project URL (required for cloud sync) |
|
|
234
|
+
| `supabaseAnonKey` | string | — | Supabase anonymous key (required for cloud sync) |
|
|
235
|
+
| `manifestPath` | string | `~/.cordenar/manifest.json` | Named Service Discovery manifest path |
|
|
236
|
+
|
|
237
|
+
### Hemisphere & Compend Integration
|
|
238
|
+
|
|
239
|
+
Cordenar requires no configuration on its side to receive events. Instead, configure Hemisphere and Compend to push events to Cordenar:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
// ~/.hemisphere/config.json
|
|
243
|
+
{ "notifySubscribers": ["cordenar"] }
|
|
244
|
+
|
|
245
|
+
// ~/.compend/config.json
|
|
246
|
+
{ "notifySubscribers": ["cordenar"] }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
When configured, every tool call in Hemisphere (`memory_store`, `memory_trash`, `memory_update`, etc.) and Compend (`compend_index`) will notify Cordenar in real time. The Cordenar manifest (`~/.cordenar/manifest.json`) is created automatically on startup and deleted on shutdown — no URL to maintain.
|
|
250
|
+
|
|
251
|
+
Hemisphere config key: `notifySubscribers` (array of service names). Env override: `HEMISPHERE_NOTIFY_SUBSCRIBERS` (comma-separated).
|
|
252
|
+
Compend config key: `notifySubscribers` (array of service names). Env override: `COMPEND_NOTIFY_SUBSCRIBERS` (comma-separated).
|
|
253
|
+
|
|
254
|
+
### Environment Variables
|
|
255
|
+
|
|
256
|
+
Environment variables override the config file and code defaults at the highest priority.
|
|
257
|
+
|
|
258
|
+
| Variable | Equivalent Config Key | Default |
|
|
259
|
+
|----------|----------------------|---------|
|
|
260
|
+
| `CORDENAR_PORT` | `port` | `3458` |
|
|
261
|
+
| `CORDENAR_DB_PATH` | `dbPath` | `~/.cordenar/sync.db` |
|
|
262
|
+
| `CORDENAR_DIR` | `cordenarDir` | `~/.cordenar` |
|
|
263
|
+
| `SUPABASE_URL` | `supabaseUrl` | — |
|
|
264
|
+
| `SUPABASE_ANON_KEY` | `supabaseAnonKey` | — |
|
|
265
|
+
|
|
266
|
+
## Project Structure
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
cordenar-mcp/
|
|
270
|
+
├── index.js MCP stdio server (6 tool handlers)
|
|
271
|
+
├── dashboard.js HTTP dashboard + SSE broadcast server
|
|
272
|
+
├── dashboard/
|
|
273
|
+
│ ├── api-handler.js Dashboard REST API routes
|
|
274
|
+
│ └── public/
|
|
275
|
+
│ ├── index.html Dashboard HTML + ARIA structure
|
|
276
|
+
│ ├── style.css Full theme (dark/light), toast, dialog, skeleton
|
|
277
|
+
│ ├── app.js Frontend SSE client, keyboard nav, WCAG 2.1 AA
|
|
278
|
+
│ └── logo.svg Network icon logo
|
|
279
|
+
├── db.js Sync database init + synapse_map CRUD
|
|
280
|
+
├── sync.js Push/pull/hash-diff/unlink logic
|
|
281
|
+
├── auth.js Device code OAuth + JWT store
|
|
282
|
+
├── supabase.js Supabase client wrapper
|
|
283
|
+
├── embedding.js MurmurHash3 → 256-dim float vector
|
|
284
|
+
├── config.js Config loader (defaults ← config.json ← env vars)
|
|
285
|
+
├── package.json
|
|
286
|
+
├── CHANGELOG.md
|
|
287
|
+
└── README.md
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
On startup, Cordenar writes `~/.cordenar/manifest.json` for Named Service Discovery. On shutdown, it is deleted automatically.
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
### Scripts
|
|
295
|
+
|
|
296
|
+
For local development (after `git clone`):
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npm start # Start dashboard server (same as `cordenar`)
|
|
300
|
+
npm run stop # Stop running instance (same as `cordenar stop`)
|
|
301
|
+
npm run restart # Stop then start
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
For installed users, the global `cordenar` CLI handles these — see [Quick Start](#quick-start).
|
|
305
|
+
|
|
306
|
+
### Testing the MCP server
|
|
307
|
+
|
|
308
|
+
Test with the MCP Inspector:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npx @modelcontextprotocol/inspector node index.js
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Roadmap
|
|
315
|
+
|
|
316
|
+
- Supabase integration (device auth, synapse push/pull, SSE revocation)
|
|
317
|
+
- Full bidirectional sync with hash diff and conflict resolution
|
|
318
|
+
- Cloud dashboard (React SPA — org management, approval queue, billing)
|
|
319
|
+
- Lemon Squeezy billing integration (annual subscriptions per node)
|
|
320
|
+
- Multi-node registration and seat management
|
|
321
|
+
- Role-based access control (Owner, Admin, Member)
|
|
322
|
+
- Tier enforcement (Team, Business, Enterprise)
|
|
323
|
+
|
|
324
|
+
## Contributing
|
|
325
|
+
|
|
326
|
+
Open an issue or PR at [github.com/hectorjarquin/cordenar-mcp](https://github.com/hectorjarquin/cordenar-mcp).
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
package/auth.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Cordenar MCP — Device code OAuth + JWT store
|
|
2
|
+
// Pattern: ~/.cordenar/auth.json holds the session
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { getConfig } from './config.js';
|
|
5
|
+
|
|
6
|
+
export function loadAuth() {
|
|
7
|
+
const cfg = getConfig();
|
|
8
|
+
try {
|
|
9
|
+
if (!existsSync(cfg.authFile)) return null;
|
|
10
|
+
const raw = readFileSync(cfg.authFile, 'utf-8');
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveAuth(data) {
|
|
18
|
+
const cfg = getConfig();
|
|
19
|
+
writeFileSync(cfg.authFile, JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clearAuth() {
|
|
23
|
+
const cfg = getConfig();
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(cfg.authFile)) {
|
|
26
|
+
writeFileSync(cfg.authFile, '');
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isAuthenticated() {
|
|
32
|
+
const auth = loadAuth();
|
|
33
|
+
if (!auth || !auth.access_token) return false;
|
|
34
|
+
if (auth.expires_at && Date.now() / 1000 > auth.expires_at) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getAccessToken() {
|
|
39
|
+
const auth = loadAuth();
|
|
40
|
+
if (!auth) return null;
|
|
41
|
+
return auth.access_token;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getNodeId() {
|
|
45
|
+
const auth = loadAuth();
|
|
46
|
+
if (!auth) return null;
|
|
47
|
+
return auth.node_id;
|
|
48
|
+
}
|
package/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Cordenar MCP — Configuration loader
|
|
2
|
+
// Pattern mirrors Hemisphere (config.js) and Compend (config.js)
|
|
3
|
+
// Priority: code defaults → config file → environment variables
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
port: 3458,
|
|
10
|
+
cordenarDir: join(homedir(), '.cordenar'),
|
|
11
|
+
dbPath: join(homedir(), '.cordenar', 'sync.db'),
|
|
12
|
+
hemisphereDb: join(homedir(), '.hemisphere', 'memories.db'),
|
|
13
|
+
compendDb: join(homedir(), '.compend', 'concepts.db'),
|
|
14
|
+
authFile: join(homedir(), '.cordenar', 'auth.json'),
|
|
15
|
+
manifestPath: join(homedir(), '.cordenar', 'manifest.json'),
|
|
16
|
+
supabaseUrl: '',
|
|
17
|
+
supabaseAnonKey: '',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
21
|
+
|
|
22
|
+
export function deepMerge(target, source) {
|
|
23
|
+
for (const key of Object.keys(source)) {
|
|
24
|
+
if (!Object.hasOwn(target, key)) {
|
|
25
|
+
target[key] = JSON.parse(JSON.stringify(source[key]));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const tv = target[key];
|
|
29
|
+
const sv = source[key];
|
|
30
|
+
if (isObject(tv) && isObject(sv)) {
|
|
31
|
+
deepMerge(tv, sv);
|
|
32
|
+
} else {
|
|
33
|
+
target[key] = sv;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readConfigFile() {
|
|
40
|
+
const path = join(homedir(), '.cordenar', 'config.json');
|
|
41
|
+
try {
|
|
42
|
+
const raw = readFileSync(path, 'utf-8');
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveTilde(p) {
|
|
50
|
+
if (typeof p !== 'string') return p;
|
|
51
|
+
if (p.startsWith('~/') || p === '~') {
|
|
52
|
+
return join(homedir(), p.slice(1));
|
|
53
|
+
}
|
|
54
|
+
return p;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applyEnvOverrides(cfg) {
|
|
58
|
+
if (process.env.SUPABASE_URL) cfg.supabaseUrl = process.env.SUPABASE_URL;
|
|
59
|
+
if (process.env.SUPABASE_ANON_KEY)
|
|
60
|
+
cfg.supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
|
|
61
|
+
if (process.env.CORDENAR_PORT)
|
|
62
|
+
cfg.port = parseInt(process.env.CORDENAR_PORT, 10) || 3458;
|
|
63
|
+
if (process.env.CORDENAR_DIR)
|
|
64
|
+
cfg.cordenarDir = resolveTilde(process.env.CORDENAR_DIR);
|
|
65
|
+
if (process.env.CORDENAR_DB_PATH)
|
|
66
|
+
cfg.dbPath = resolveTilde(process.env.CORDENAR_DB_PATH);
|
|
67
|
+
return cfg;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let _cfg = null;
|
|
71
|
+
|
|
72
|
+
export function getConfig() {
|
|
73
|
+
if (_cfg) return _cfg;
|
|
74
|
+
const defaults = JSON.parse(JSON.stringify(DEFAULTS));
|
|
75
|
+
_cfg = deepMerge(defaults, readConfigFile());
|
|
76
|
+
applyEnvOverrides(_cfg);
|
|
77
|
+
return _cfg;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getDbPath() {
|
|
81
|
+
const cfg = getConfig();
|
|
82
|
+
const dir = join(cfg.dbPath, '..');
|
|
83
|
+
if (!existsSync(dir)) {
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
return cfg.dbPath;
|
|
87
|
+
}
|