Skip to content

Commit e9cafe6

Browse files
committed
several fixes
1 parent 1162ba2 commit e9cafe6

18 files changed

Lines changed: 510 additions & 254 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
A CRDT-based, offline-capable, real-time-syncable **tree-of-data** library for Browser and Node.js.
44

5+
## work-in-progress - please ignore for the moment
6+
57
Data Items are organised in a hierarchy (with optional links between them), can carry arbitrary typed values (text, binary, large blobs), and synchronise across devices and users without conflicts — even when peers are offline for extended periods. The library is split into small, composable packages so you can use only what you need.
68

79
The CRDT engine is **pluggable**: choose from three ready-made backends or write your own: [Y.js](https://yjs.dev/), [Loro CRDT](https://loro.dev/) or [JSON JOY](https://jsonjoy.com/).
@@ -64,7 +66,7 @@ All three backend packages expose an **identical public API**. Import from which
6466
| `@rozek/sds-sidecar-loro` | `sds-sidecar-loro` | Loro |
6567
| `@rozek/sds-sidecar-yjs` | `sds-sidecar-yjs` | Y.js |
6668

67-
Each wrapper is a thin ~40-line entry point that wires the corresponding `@rozek/sds-core-*` backend into the generic library. Install only the package matching your chosen CRDT backend.
69+
Each wrapper is a thin \~40-line entry point that wires the corresponding `@rozek/sds-core-*` backend into the generic library. Install only the package matching your chosen CRDT backend.
6870

6971
---
7072

packages/sds-command-jj/src/tests/cli.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('CLI default behaviour (CL)', () => {
4747
it('CL-06: "help entry" shows entry subcommand help (not top-level help)', async () => {
4848
const Result = await runCLI(['help', 'entry'])
4949
expect(Result.ExitCode).toBe(0)
50-
expect(Result.Stdout).toMatch(/sds entry/) // entry-specific usage line
50+
expect(Result.Stdout).toMatch(/sds\S* entry/) // entry-specific usage line
5151
expect(Result.Stdout).toMatch(/create/) // at least one entry subcommand listed
5252
expect(Result.Stderr).not.toMatch(/error/)
5353
})

packages/sds-command/dist/sds-command.d.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
/*******************************************************************************
2-
* *
3-
* SDS Command CLI *
4-
* *
5-
*******************************************************************************/
1+
import { SDS_StoreFactory } from './StoreAccess.js';
62
export type { SDS_StoreFactory } from './StoreAccess.js';
73
/**** runCommand — called by backend-specific wrapper packages ****/
84
export declare function runCommand(StoreFactory: SDS_StoreFactory, CommandName?: string, Version?: string): Promise<void>;

packages/sds-command/dist/sds-command.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sds-command/dist/sds-command.js

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ function C(e, n, t, o = []) {
354354
for (const [r, s] of Object.entries(t))
355355
e[r] = s;
356356
for (const r of o)
357-
e[r] = void 0;
357+
delete e[r];
358358
}
359359
function Z(e) {
360360
const n = [];
@@ -1258,24 +1258,27 @@ async function V(e, n) {
12581258
}
12591259
}
12601260
async function ze() {
1261-
const { CleanArgv: e, InfoEntries: n } = N(process.argv.slice(2)), t = Object.entries(n).flatMap(([r, s]) => [
1262-
`--info.${r}`,
1263-
JSON.stringify(s)
1264-
]), o = O(t);
1265-
G(o);
1261+
const { CleanArgv: e, InfoEntries: n, InfoDeleteKeys: t } = N(process.argv.slice(2)), o = [
1262+
...Object.entries(n).flatMap(([s, i]) => [
1263+
`--info.${s}`,
1264+
JSON.stringify(i)
1265+
]),
1266+
...t.map((s) => `--info-delete.${s}`)
1267+
], r = O(o);
1268+
G(r);
12661269
try {
1267-
await o.parseAsync(["node", E, ...e]);
1268-
} catch (r) {
1269-
const s = r;
1270-
if ((s.code === "commander.help" || s.code === "commander.helpDisplayed" || s.code === "commander.version") && process.exit(c.OK), (s.code === "commander.unknownCommand" || s.code === "commander.unknownOption" || s.code === "commander.missingArgument" || s.code === "commander.missingMandatoryOptionValue") && (process.stderr.write(`${E}: ${s.message}
1270+
await r.parseAsync(["node", E, ...e]);
1271+
} catch (s) {
1272+
const i = s;
1273+
if ((i.code === "commander.help" || i.code === "commander.helpDisplayed" || i.code === "commander.version") && process.exit(c.OK), (i.code === "commander.unknownCommand" || i.code === "commander.unknownOption" || i.code === "commander.missingArgument" || i.code === "commander.missingMandatoryOptionValue") && (process.stderr.write(`${E}: ${i.message}
12711274
1272-
`), process.stderr.write(o.helpInformation()), process.exit(c.UsageError)), r instanceof R && (process.stderr.write(`${E}: ${r.message}
1273-
`), process.exit(r.ExitCode)), r instanceof l) {
1274-
const a = w({});
1275-
P(a, r.message, r.ExitCode), process.exit(r.ExitCode);
1275+
`), process.stderr.write(r.helpInformation()), process.exit(c.UsageError)), s instanceof R && (process.stderr.write(`${E}: ${s.message}
1276+
`), process.exit(s.ExitCode)), s instanceof l) {
1277+
const d = w({});
1278+
P(d, s.message, s.ExitCode), process.exit(s.ExitCode);
12761279
}
1277-
const i = w({});
1278-
P(i, r.message ?? String(r)), process.exit(c.GeneralError);
1280+
const a = w({});
1281+
P(a, s.message ?? String(s)), process.exit(c.GeneralError);
12791282
}
12801283
}
12811284
async function ot(e, n = "sds", t = "0.0.0") {

packages/sds-command/src/InfoParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,6 @@ export function applyInfoToEntry (
144144

145145
// --info-delete.key: remove individual keys (already validated in extractInfoEntries)
146146
for (const Key of InfoDeleteKeys) {
147-
InfoProxy[Key] = undefined
147+
delete InfoProxy[Key]
148148
}
149149
}

packages/sds-command/src/sds-command.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Command } from 'commander'
1212
import { resolveConfig, SDS_ConfigError } from './Config.js'
1313
import { printError } from './Output.js'
1414
import { ExitCodes } from './ExitCodes.js'
15-
import { SDS_CommandError, setStoreFactory } from './StoreAccess.js'
15+
import { SDS_CommandError, setStoreFactory, type SDS_StoreFactory } from './StoreAccess.js'
1616
import { extractInfoEntries } from './InfoParser.js'
1717
import { startREPL } from './REPL.js'
1818
import { runScript } from './ScriptRunner.js'
@@ -216,13 +216,16 @@ async function executeTokens (
216216
/**** main — CLI entry point ****/
217217

218218
async function main ():Promise<void> {
219-
// strip --info.<key> options before commander sees them; keep them for
220-
// later injection into sub-command handlers via buildProgram(ExtraArgv)
221-
const { CleanArgv, InfoEntries } = extractInfoEntries(process.argv.slice(2))
219+
// strip --info.<key> and --info-delete.<key> options before commander sees them;
220+
// keep them for later injection into sub-command handlers via buildProgram(ExtraArgv)
221+
const { CleanArgv, InfoEntries, InfoDeleteKeys } = extractInfoEntries(process.argv.slice(2))
222222

223-
const ExtraArgv = Object.entries(InfoEntries).flatMap(([Key, Value]) => [
224-
`--info.${Key}`, JSON.stringify(Value),
225-
])
223+
const ExtraArgv = [
224+
...Object.entries(InfoEntries).flatMap(([Key, Value]) => [
225+
`--info.${Key}`, JSON.stringify(Value),
226+
]),
227+
...InfoDeleteKeys.map((Key) => `--info-delete.${Key}`),
228+
]
226229

227230
const Program = buildProgram(ExtraArgv)
228231
applyExitOverride(Program)

packages/sds-command/src/tests/Config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ describe('resolveConfig / DBPathFor', () => {
6969
})
7070

7171
it('CF-07: DBPathFor combines PersistenceDir and store ID into a .db path', () => {
72-
const Config = resolveConfig({ dataDir:'/tmp/sds' })
72+
const Config = resolveConfig({ persistenceDir:'/tmp/sds' })
7373
expect(DBPathFor(Config, 'valid-id_123')).toBe('/tmp/sds/valid-id_123.db')
7474
})
7575

7676
it('CF-08: DBPathFor replaces chars outside [a-zA-Z0-9_-] in the store ID with _', () => {
77-
const Config = resolveConfig({ dataDir:'/tmp/sds' })
77+
const Config = resolveConfig({ persistenceDir:'/tmp/sds' })
7878
expect(DBPathFor(Config, 'my/store')).toBe('/tmp/sds/my_store.db')
7979
expect(DBPathFor(Config, 'test store')).toBe('/tmp/sds/test_store.db')
8080
})
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# @rozek/sds-mcp-server-jj
2+
3+
MCP (Model Context Protocol) server for the **shareable-data-store** (SDS) family — backed by the [json-joy](https://github.com/streamich/json-joy) CRDT engine. Exposes all SDS store and entry operations as MCP tools so that AI agents can manage CRDT stores, entries, items, links, tokens, and batch workflows through the standard MCP tool-calling interface.
4+
5+
This package is the **runnable binary**. The tool definitions, handlers, and shared infrastructure live in [`@rozek/sds-mcp-server`](../sds-mcp-server/README.md); this package wires in the json-joy store factory and exposes the result as the `sds-mcp-server-jj` executable.
6+
7+
---
8+
9+
## Prerequisites
10+
11+
| requirement | details |
12+
| --- | --- |
13+
| **Node.js 22.5+** | required to run the server. Download from [nodejs.org](https://nodejs.org). |
14+
| **MCP client** | any host that speaks the Model Context Protocol (Claude Desktop, custom agent, etc.) |
15+
| **SDS server** | only required for network operations (`sds_store_sync`, `sds_store_ping`, `sds_token_issue`). All local read/write tools work entirely offline against the SQLite file. |
16+
| **JWT tokens** | server operations need a client token (scope `read` or `write`) or an admin token (scope `admin`). Tokens are issued by the server's `POST /api/token` endpoint via `sds_token_issue`. |
17+
18+
SQLite support is built directly into Node.js since version 22.5 via the `node:sqlite` module — no separate database driver, no native C++ addon, and no build toolchain is needed. The server binary therefore runs with **Node.js as its only dependency**.
19+
20+
> **Note on stability:** `node:sqlite` is classified as *Stability 1 — Experimental* in the Node.js 22 and 24 documentation. In practice the API has been stable since its introduction. The server suppresses the associated runtime warning automatically.
21+
22+
---
23+
24+
## Installation
25+
26+
```bash
27+
npm add @rozek/sds-mcp-server-jj
28+
```
29+
30+
After installation the `sds-mcp-server-jj` command is available in the project's `node_modules/.bin/` directory.
31+
32+
---
33+
34+
## Running without Installation
35+
36+
The package includes a `bin` entry, so it can be started directly via `npx` — no prior installation required:
37+
38+
```bash
39+
npx @rozek/sds-mcp-server-jj
40+
```
41+
42+
This is the recommended way to configure MCP hosts that manage servers themselves. Register the server in your MCP host configuration using `npx` as the command:
43+
44+
**Claude Desktop (`claude_desktop_config.json`):**
45+
46+
```json
47+
{
48+
"mcpServers": {
49+
"sds": {
50+
"command": "npx",
51+
"args": [ "-y", "@rozek/sds-mcp-server-jj" ]
52+
}
53+
}
54+
}
55+
```
56+
57+
The `-y` flag skips the confirmation prompt when `npx` downloads the package. Remove it if you prefer to confirm each download.
58+
59+
> **Note on Node.js version:** `npx` uses whatever `node` is on your `PATH`. Make sure it is version 22.5 or later before adding the server to a host configuration.
60+
61+
---
62+
63+
## Configuration (installed package)
64+
65+
If you have installed `@rozek/sds-mcp-server-jj` locally or globally, register the built binary directly in your MCP host configuration:
66+
67+
**Claude Desktop (`claude_desktop_config.json`):**
68+
69+
```json
70+
{
71+
"mcpServers": {
72+
"sds": {
73+
"command": "node",
74+
"args": [ "/path/to/node_modules/@rozek/sds-mcp-server-jj/dist/sds-mcp-server-jj.js" ]
75+
}
76+
}
77+
}
78+
```
79+
80+
The server communicates over **stdio** — no port, no HTTP server, no daemon is required.
81+
82+
### Server-level defaults
83+
84+
You can pre-configure default values for `StoreId`, `PersistenceDir`, `ServerURL`, `Token`, and `AdminToken` so that individual tool calls do not need to repeat them. There are two ways to do this — they can be combined freely, and **tool parameters always take precedence** over both.
85+
86+
**CLI arguments** (passed in the `args` array of the MCP host config):
87+
88+
| argument | applies to |
89+
| --- | --- |
90+
| `--store <id>` | default `StoreId` for all tools |
91+
| `--persistence-dir <path>` | default `PersistenceDir` for all tools |
92+
| `--server <url>` | default `ServerURL` for sync/token tools |
93+
| `--token <jwt>` | default client `Token` for sync tools |
94+
| `--admin-token <jwt>` | default `AdminToken` for `sds_token_issue` |
95+
96+
**Environment variables** (set in the `env` block of the MCP host config, or in the shell):
97+
98+
| variable | applies to |
99+
| --- | --- |
100+
| `SDS_STORE_ID` | default `StoreId` for all tools |
101+
| `SDS_PERSISTENCE_DIR` | default `PersistenceDir` for all tools |
102+
| `SDS_SERVER_URL` | default `ServerURL` for sync/token tools |
103+
| `SDS_TOKEN` | default client `Token` for sync tools |
104+
| `SDS_ADMIN_TOKEN` | default `AdminToken` for `sds_token_issue` |
105+
106+
**Precedence** (highest to lowest): tool parameter → CLI argument → environment variable → built-in default (`~/.sds` for `PersistenceDir`).
107+
108+
**Example — fixed store and data directory via CLI args:**
109+
110+
```json
111+
{
112+
"mcpServers": {
113+
"sds": {
114+
"command": "node",
115+
"args": [
116+
"/path/to/node_modules/@rozek/sds-mcp-server-jj/dist/sds-mcp-server-jj.js",
117+
"--store", "my-notes",
118+
"--persistence-dir", "/home/alice/.my-sds-data"
119+
]
120+
}
121+
}
122+
}
123+
```
124+
125+
**Example — sync server and token via environment variables:**
126+
127+
```json
128+
{
129+
"mcpServers": {
130+
"sds": {
131+
"command": "node",
132+
"args": [ "/path/to/.../sds-mcp-server-jj.js" ],
133+
"env": {
134+
"SDS_SERVER_URL": "wss://sync.example.com",
135+
"SDS_TOKEN": "eyJhbGci..."
136+
}
137+
}
138+
}
139+
}
140+
```
141+
142+
---
143+
144+
## Tool reference
145+
146+
This package exposes 20 tools grouped into six categories. Full documentation for every tool — parameters, return values, error conditions, and examples — is in the [`@rozek/sds-mcp-server` README](../sds-mcp-server/README.md#tool-reference).
147+
148+
| category | tools |
149+
| --- | --- |
150+
| **store** | `sds_store_info`, `sds_store_ping`, `sds_store_sync`, `sds_store_destroy`, `sds_store_export`, `sds_store_import` |
151+
| **entry** | `sds_entry_create`, `sds_entry_get`, `sds_entry_list`, `sds_entry_update`, `sds_entry_move`, `sds_entry_delete`, `sds_entry_restore`, `sds_entry_purge` |
152+
| **trash** | `sds_trash_list`, `sds_trash_purge_all`, `sds_trash_purge_expired` |
153+
| **tree** | `sds_tree_show` |
154+
| **token** | `sds_token_issue` |
155+
| **batch** | `sds_batch` |
156+
157+
---
158+
159+
## Examples
160+
161+
### Local store — first steps
162+
163+
```
164+
sds_entry_create { StoreId: "notes", Label: "Recipes", MIMEType: "text/plain", Value: "Pasta: boil, sauce, enjoy" }
165+
sds_tree_show { StoreId: "notes" }
166+
sds_entry_list { StoreId: "notes", Id: "root", Fields: ["Label", "MIMEType"] }
167+
sds_entry_get { StoreId: "notes", Id: "<uuid>" }
168+
```
169+
170+
### Batch setup
171+
172+
Initialise a folder structure in one round trip:
173+
174+
```
175+
sds_batch {
176+
StoreId: "team-wiki",
177+
Commands: [
178+
{ Tool: "sds_entry_create", Params: { Label: "Documentation", MIMEType: "text/plain" } },
179+
{ Tool: "sds_entry_create", Params: { Label: "Meeting Notes", MIMEType: "text/plain" } },
180+
{ Tool: "sds_entry_create", Params: { Label: "Decisions", MIMEType: "text/plain" } }
181+
]
182+
}
183+
```
184+
185+
### Export and restore
186+
187+
```
188+
sds_store_export { StoreId: "notes", Encoding: "json", OutputFile: "/backups/notes.json" }
189+
sds_store_import { StoreId: "notes-restored", InputFile: "/backups/notes.json", InputEncoding: "json" }
190+
```
191+
192+
---
193+
194+
## License
195+
196+
[MIT License](../../LICENSE.md) © Andreas Rozek

packages/sds-mcp-server-jj/TestCases.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ All tools are called via the MCP protocol through `StdioClientTransport`. Every
102102
| EL-05 | `recursive: true`, `Depth: 1` | nested children of top-level items not included |
103103
| EL-06 | `Fields: ["Label"]` | every entry has a `Label` field |
104104
| EL-07 | `InfoKeys: ["tag"]` | every entry has an `Info` object with only the `tag` key |
105+
| EL-08 | `only: "foobar"` (invalid value) | `isError: true`; message contains `'only'` |
105106
| EL-09 | link ID as container | `isError: true` |
106107
| EL-10 | `Id: "root"` output | Trash ID absent from result array |
107108
| EL-11 | `Id: "lost-and-found"` | alias resolves; `isError: false`; result is an array |
@@ -206,6 +207,8 @@ All tools are called via the MCP protocol through `StdioClientTransport`. Every
206207
| BA-06 | sync step with unreachable server, `onError: "continue"` | sync step `ok: false`; subsequent create step `ok: true` |
207208
| BA-07 | `StoreId` + `PersistenceDir` at batch level | commands without them use the correct store |
208209
| BA-08 | `sds_token_issue` in Commands (disallowed) | `isError: true` |
210+
| BA-09 | `sds_store_ping` in Commands (disallowed) | `isError: true` before any execution |
211+
| BA-10 | batch `sds_store_export` (JSON, no file) + `sds_store_import` (base64) into new store | both steps `ok: true`; imported store `EntryCount` equals exported |
209212

210213
## CF — Config Defaults
211214

0 commit comments

Comments
 (0)