@appsoftwareltd/etherpk-mcp 0.1.1 → 0.2.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.
- package/README.md +33 -4
- package/dist/main.js +87 -24
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,18 +17,18 @@ server and graph filled in. They are:
|
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
# Once per computer. Prompts for an account-wide Personal Access Token (make one at
|
|
20
|
-
# https://
|
|
20
|
+
# https://sync.etherpk.com/account/tokens, also reachable from "Access tokens" in
|
|
21
21
|
# EtherPK's account menu), then shows a short code and the address of your EtherPK:
|
|
22
22
|
# open EtherPK there in a browser where you're signed in with your graphs unlocked -
|
|
23
23
|
# any page will do - and confirm the code. The same step as adding a phone.
|
|
24
|
-
npx @appsoftwareltd/etherpk-mcp login --server https://
|
|
24
|
+
npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.etherpk.com
|
|
25
25
|
|
|
26
26
|
# Self-hosting your own Sync Server? Give its address instead:
|
|
27
|
-
# npx @appsoftwareltd/etherpk-mcp login --server https://sync.your-domain.example
|
|
27
|
+
# npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.your-domain.example
|
|
28
28
|
|
|
29
29
|
# No EtherPK to hand on this computer (a server you reach over SSH, say)? Press r while
|
|
30
30
|
# login is waiting, or use your Recovery Code from the start:
|
|
31
|
-
# npx @appsoftwareltd/etherpk-mcp login --server https://
|
|
31
|
+
# npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.etherpk.com --recovery-code
|
|
32
32
|
|
|
33
33
|
# Tell the agent about the graph (Claude Code shown; the Agents tab has the others).
|
|
34
34
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <graph id>
|
|
@@ -37,6 +37,10 @@ claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <graph i
|
|
|
37
37
|
`npx @appsoftwareltd/etherpk-mcp graphs` lists the graphs the token can reach, by name and id.
|
|
38
38
|
For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the prompts.
|
|
39
39
|
|
|
40
|
+
Every command runs through `npx`, which fetches the package but never puts `etherpk-mcp` on your
|
|
41
|
+
PATH. If you'd rather type the short form, `npm install -g @appsoftwareltd/etherpk-mcp` once and
|
|
42
|
+
drop the `npx @appsoftwareltd/` prefix; the program's own hints follow whichever way you ran it.
|
|
43
|
+
|
|
40
44
|
## What the agent gets
|
|
41
45
|
|
|
42
46
|
`list_documents`, `read_document`, `search`, `backlinks`, `tasks`, `edit_document` (an exact,
|
|
@@ -57,5 +61,30 @@ One running instance serves one graph.
|
|
|
57
61
|
Revoke the token at the portal to cut the agent off, and `npx @appsoftwareltd/etherpk-mcp logout`
|
|
58
62
|
to forget the keys and delete the cache on that computer.
|
|
59
63
|
|
|
64
|
+
## Building and publishing
|
|
65
|
+
|
|
66
|
+
From the EtherPK monorepo (`apps/mcp`; the bundle compiles the Client's own sync, crypto and
|
|
67
|
+
index code in through a `$lib` alias, so it builds from the repo, not from this folder alone):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pnpm install # once, at the repo root
|
|
71
|
+
pnpm --filter @appsoftwareltd/etherpk-mcp check # type-check
|
|
72
|
+
pnpm --filter @appsoftwareltd/etherpk-mcp test # unit tests (loopback relay, no server)
|
|
73
|
+
pnpm --filter @appsoftwareltd/etherpk-mcp build # dist/main.js; run it with node dist/main.js
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To release: bump `version` in `package.json` (the CLI reads its version from there), then from
|
|
77
|
+
`apps/mcp` in a real terminal (both the login and the publish need a browser or one-time code,
|
|
78
|
+
which needs a TTY):
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npm whoami || npm login # only if not already signed in (or the token has expired); needs publish rights on @appsoftwareltd
|
|
82
|
+
pnpm publish --access public --no-git-checks # prepack runs the build; pnpm rewrites workspace:* deps
|
|
83
|
+
npx -y @appsoftwareltd/etherpk-mcp@<version> --version # verify, once the registry lists it (a few minutes)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The end-to-end specs in `tests-sync/agents/` build and drive this bundle against a real Sync
|
|
87
|
+
Server; the technical notes are in the repo under `docs/docs/technical/Headless Client.md`.
|
|
88
|
+
|
|
60
89
|
Full guide, including how the cache stays current and what an agent can and can't do:
|
|
61
90
|
<https://docs.etherpk.com/using-ai-agents-with-your-notes>.
|
package/dist/main.js
CHANGED
|
@@ -15,6 +15,61 @@ import { parser } from "@lezer/markdown";
|
|
|
15
15
|
import { parse, stringify } from "yaml";
|
|
16
16
|
import { deserialize, serialize } from "node:v8";
|
|
17
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
var package_default = {
|
|
19
|
+
name: "@appsoftwareltd/etherpk-mcp",
|
|
20
|
+
version: "0.2.0",
|
|
21
|
+
license: "Elastic-2.0",
|
|
22
|
+
description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
|
|
23
|
+
type: "module",
|
|
24
|
+
"private": false,
|
|
25
|
+
bin: { "etherpk-mcp": "./bin/etherpk-mcp.js" },
|
|
26
|
+
files: ["bin", "dist"],
|
|
27
|
+
scripts: {
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"prepack": "pnpm build"
|
|
33
|
+
},
|
|
34
|
+
dependencies: {
|
|
35
|
+
"@lezer/markdown": "^1.6.4",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
37
|
+
"@noble/curves": "^2.2.0",
|
|
38
|
+
"@noble/hashes": "^2.2.0",
|
|
39
|
+
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
40
|
+
"fake-indexeddb": "^6.2.5",
|
|
41
|
+
"lib0": "^0.2.117",
|
|
42
|
+
"y-protocols": "^1.0.7",
|
|
43
|
+
"yaml": "^2.9.0",
|
|
44
|
+
"yjs": "^13.6.31",
|
|
45
|
+
"zod": "^4.3.6"
|
|
46
|
+
},
|
|
47
|
+
devDependencies: {
|
|
48
|
+
"@appsoftwareltd/etherpk-shared": "workspace:*",
|
|
49
|
+
"@codemirror/view": "^6.43.0",
|
|
50
|
+
"@types/node": "^25.6.0",
|
|
51
|
+
"typescript": "^6.0.2",
|
|
52
|
+
"vite": "^8.0.8",
|
|
53
|
+
"vitest": "^4.1.4"
|
|
54
|
+
},
|
|
55
|
+
publishConfig: { "access": "public" },
|
|
56
|
+
homepage: "https://docs.etherpk.com/using-ai-agents-with-your-notes",
|
|
57
|
+
repository: {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/appsoftwareltd/etherpk",
|
|
60
|
+
"directory": "apps/mcp"
|
|
61
|
+
},
|
|
62
|
+
keywords: [
|
|
63
|
+
"etherpk",
|
|
64
|
+
"mcp",
|
|
65
|
+
"model-context-protocol",
|
|
66
|
+
"knowledge-graph",
|
|
67
|
+
"notes",
|
|
68
|
+
"agent"
|
|
69
|
+
],
|
|
70
|
+
engines: { "node": ">=22" }
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
18
73
|
//#region ../client/src/lib/crypto/bytes.ts
|
|
19
74
|
/**
|
|
20
75
|
* Byte-array and encoding primitives for the crypto core. No cryptography here.
|
|
@@ -598,13 +653,13 @@ function createSyncTokenSource(mint, opts) {
|
|
|
598
653
|
*/
|
|
599
654
|
function createHeadlessAccount(config) {
|
|
600
655
|
const api = createSyncApi({
|
|
601
|
-
baseUrl: config.
|
|
656
|
+
baseUrl: config.syncServer,
|
|
602
657
|
token: config.pat
|
|
603
658
|
});
|
|
604
659
|
return {
|
|
605
660
|
api,
|
|
606
|
-
serverBaseUrl: config.
|
|
607
|
-
relayUrl: relayUrlFrom(config.
|
|
661
|
+
serverBaseUrl: config.syncServer,
|
|
662
|
+
relayUrl: relayUrlFrom(config.syncServer),
|
|
608
663
|
tokenFor: (graphId) => createSyncTokenSource(() => api.mintSyncToken(graphId))
|
|
609
664
|
};
|
|
610
665
|
}
|
|
@@ -674,9 +729,9 @@ function parseConfig(raw) {
|
|
|
674
729
|
}
|
|
675
730
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
676
731
|
const rec = parsed;
|
|
677
|
-
if (typeof rec.
|
|
732
|
+
if (typeof rec.syncServer !== "string" || typeof rec.pat !== "string") return null;
|
|
678
733
|
const out = {
|
|
679
|
-
|
|
734
|
+
syncServer: rec.syncServer.replace(/\/$/, ""),
|
|
680
735
|
pat: rec.pat
|
|
681
736
|
};
|
|
682
737
|
if (typeof rec.vaultKey === "string" && rec.vaultKey !== "") out.vaultKey = rec.vaultKey;
|
|
@@ -8335,7 +8390,7 @@ function createMcpServer(graph, info) {
|
|
|
8335
8390
|
/**
|
|
8336
8391
|
* `etherpk-mcp`: the [[Headless Client]]'s command line (ADR 0072).
|
|
8337
8392
|
*
|
|
8338
|
-
* etherpk-mcp login --server <url> [--pat <token>] [--recovery-code]
|
|
8393
|
+
* etherpk-mcp login --sync-server <url> [--pat <token>] [--recovery-code]
|
|
8339
8394
|
* etherpk-mcp graphs
|
|
8340
8395
|
* etherpk-mcp serve --graph <id or name>
|
|
8341
8396
|
* etherpk-mcp logout
|
|
@@ -8343,22 +8398,29 @@ function createMcpServer(graph, info) {
|
|
|
8343
8398
|
* `serve` speaks MCP over stdio, so everything for the human goes to stderr; stdout belongs
|
|
8344
8399
|
* to the agent. `login` and `graphs` are interactive and print to stdout.
|
|
8345
8400
|
*/
|
|
8346
|
-
var VERSION =
|
|
8401
|
+
var VERSION = package_default.version;
|
|
8402
|
+
/**
|
|
8403
|
+
* How the user invokes this program, so every hint is one they can paste. Through npx - the way
|
|
8404
|
+
* the Agents tab, the README and the docs all say - the bin is never on their PATH, and an npx
|
|
8405
|
+
* run executes out of npm's `_npx` cache; a global install (`npm install -g`) runs from
|
|
8406
|
+
* anywhere else and does have `etherpk-mcp` on the PATH, so it gets the short spelling.
|
|
8407
|
+
*/
|
|
8408
|
+
var CMD = /[\\/]_npx[\\/]/.test(process.argv[1] ?? "") ? "npx @appsoftwareltd/etherpk-mcp" : "etherpk-mcp";
|
|
8347
8409
|
var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synced graph)
|
|
8348
8410
|
|
|
8349
|
-
|
|
8411
|
+
${CMD} login --sync-server <url> [--pat <token>] [--recovery-code]
|
|
8350
8412
|
Sign this machine in as a device of your account. Prompts for a Personal Access
|
|
8351
8413
|
Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)
|
|
8352
8414
|
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:
|
|
8353
8415
|
open EtherPK in a browser signed in to the account with its graphs unlocked and
|
|
8354
8416
|
confirm the code shown. Press r while waiting, or pass --recovery-code, to type
|
|
8355
8417
|
your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).
|
|
8356
|
-
|
|
8418
|
+
${CMD} graphs
|
|
8357
8419
|
List the synced graphs this account can reach, by name and id.
|
|
8358
|
-
|
|
8420
|
+
${CMD} serve --graph <id or name>
|
|
8359
8421
|
Serve one graph to an agent over stdio. For Claude Code:
|
|
8360
8422
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <id>
|
|
8361
|
-
|
|
8423
|
+
${CMD} logout
|
|
8362
8424
|
Forget the token, keys and cached graphs on this machine.
|
|
8363
8425
|
|
|
8364
8426
|
The config file is ${defaultConfigPath()} (override with ETHERPK_MCP_CONFIG); cached graphs live
|
|
@@ -8409,30 +8471,30 @@ async function ask(question, { secret = false } = {}) {
|
|
|
8409
8471
|
}
|
|
8410
8472
|
async function requireConfig(path) {
|
|
8411
8473
|
const config = await readConfig(path);
|
|
8412
|
-
if (!config) fail(`Not logged in on this machine. Run:
|
|
8474
|
+
if (!config) fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`);
|
|
8413
8475
|
return config;
|
|
8414
8476
|
}
|
|
8415
8477
|
async function login(args) {
|
|
8416
8478
|
const path = defaultConfigPath();
|
|
8417
|
-
const
|
|
8418
|
-
if (!/^https?:\/\//.test(
|
|
8419
|
-
const pat = args.pat ?? process.env.ETHERPK_PAT ?? await ask(`Personal Access Token (account-wide, from ${
|
|
8479
|
+
const syncServer = (args["sync-server"] ?? (await readConfig(path))?.syncServer ?? await ask("Sync Server URL: ")).replace(/\/$/, "");
|
|
8480
|
+
if (!/^https?:\/\//.test(syncServer)) fail("The Sync Server must be an http(s) URL.");
|
|
8481
|
+
const pat = args.pat ?? process.env.ETHERPK_PAT ?? await ask(`Personal Access Token (account-wide, from ${syncServer}/account/tokens): `, { secret: true });
|
|
8420
8482
|
if (!pat) fail("A Personal Access Token is required.");
|
|
8421
8483
|
const account = await connectAccount({
|
|
8422
|
-
|
|
8484
|
+
syncServer,
|
|
8423
8485
|
pat
|
|
8424
8486
|
});
|
|
8425
|
-
console.log(`Signed in to ${
|
|
8487
|
+
console.log(`Signed in to ${syncServer} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`);
|
|
8426
8488
|
const byRecoveryCode = async () => unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? await ask("Recovery Code: ", { secret: true }));
|
|
8427
8489
|
const vaultKey = args["recovery-code"] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode);
|
|
8428
8490
|
await writeConfig(path, {
|
|
8429
|
-
|
|
8491
|
+
syncServer,
|
|
8430
8492
|
pat,
|
|
8431
8493
|
vaultKey: toBase64Url(vaultKey)
|
|
8432
8494
|
});
|
|
8433
8495
|
console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`);
|
|
8434
8496
|
await listGraphs({
|
|
8435
|
-
|
|
8497
|
+
syncServer,
|
|
8436
8498
|
pat,
|
|
8437
8499
|
vaultKey: toBase64Url(vaultKey)
|
|
8438
8500
|
});
|
|
@@ -8485,7 +8547,7 @@ async function approveOrFallBack(account, byRecoveryCode) {
|
|
|
8485
8547
|
return byRecoveryCode();
|
|
8486
8548
|
}
|
|
8487
8549
|
async function listGraphs(config) {
|
|
8488
|
-
if (!config.vaultKey) fail(
|
|
8550
|
+
if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
|
|
8489
8551
|
const account = await connectAccount(config);
|
|
8490
8552
|
const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
|
|
8491
8553
|
const graphs = await account.api.listGraphs();
|
|
@@ -8507,13 +8569,14 @@ async function listGraphs(config) {
|
|
|
8507
8569
|
console.log(` ${record.id} ${label} [${record.role}]`);
|
|
8508
8570
|
}
|
|
8509
8571
|
console.log("");
|
|
8510
|
-
console.log(
|
|
8572
|
+
console.log(`Serve one to an agent with: ${CMD} serve --graph <id>`);
|
|
8573
|
+
console.log(`For Claude Code: claude mcp add etherpk -- ${CMD} serve --graph <id>`);
|
|
8511
8574
|
}
|
|
8512
8575
|
async function serve(args) {
|
|
8513
8576
|
const wanted = args.graph?.trim();
|
|
8514
8577
|
if (!wanted) fail("serve needs --graph <id or name>.");
|
|
8515
8578
|
const config = await requireConfig(defaultConfigPath());
|
|
8516
|
-
if (!config.vaultKey) fail(
|
|
8579
|
+
if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
|
|
8517
8580
|
const account = await connectAccount(config);
|
|
8518
8581
|
const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
|
|
8519
8582
|
const graphs = await account.api.listGraphs();
|
|
@@ -8535,7 +8598,7 @@ async function serve(args) {
|
|
|
8535
8598
|
break;
|
|
8536
8599
|
}
|
|
8537
8600
|
}
|
|
8538
|
-
if (!graphId) fail(`No synced graph is named or identified by "${wanted}". Run:
|
|
8601
|
+
if (!graphId) fail(`No synced graph is named or identified by "${wanted}". Run: ${CMD} graphs`);
|
|
8539
8602
|
const { record, keyring } = resolveGraphById(graphs, vault, graphId);
|
|
8540
8603
|
console.error(`etherpk-mcp: opening graph ${graphId} on ${account.serverBaseUrl}…`);
|
|
8541
8604
|
const graph = await openHeadlessGraph({
|
|
@@ -8579,7 +8642,7 @@ async function main() {
|
|
|
8579
8642
|
args: process.argv.slice(2),
|
|
8580
8643
|
allowPositionals: true,
|
|
8581
8644
|
options: {
|
|
8582
|
-
server: { type: "string" },
|
|
8645
|
+
"sync-server": { type: "string" },
|
|
8583
8646
|
pat: { type: "string" },
|
|
8584
8647
|
"recovery-code": { type: "boolean" },
|
|
8585
8648
|
graph: { type: "string" },
|