@appsoftwareltd/etherpk-mcp 0.1.0 → 0.1.2
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 +55 -11
- package/dist/main.js +158 -20
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,18 +16,30 @@ Open the graph in EtherPK, pick **Settings → Agents**, and copy the two comman
|
|
|
16
16
|
server and graph filled in. They are:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
# Once per computer. Prompts for an account-wide Personal Access Token (
|
|
20
|
-
#
|
|
21
|
-
# EtherPK
|
|
22
|
-
|
|
19
|
+
# Once per computer. Prompts for an account-wide Personal Access Token (make one at
|
|
20
|
+
# https://server.etherpk.com/account/tokens, also reachable from "Access tokens" in
|
|
21
|
+
# EtherPK's account menu), then shows a short code and the address of your EtherPK:
|
|
22
|
+
# open EtherPK there in a browser where you're signed in with your graphs unlocked -
|
|
23
|
+
# any page will do - and confirm the code. The same step as adding a phone.
|
|
24
|
+
npx @appsoftwareltd/etherpk-mcp login --server https://server.etherpk.com
|
|
25
|
+
|
|
26
|
+
# Self-hosting your own Sync Server? Give its address instead:
|
|
27
|
+
# npx @appsoftwareltd/etherpk-mcp login --server https://sync.your-domain.example
|
|
28
|
+
|
|
29
|
+
# No EtherPK to hand on this computer (a server you reach over SSH, say)? Press r while
|
|
30
|
+
# login is waiting, or use your Recovery Code from the start:
|
|
31
|
+
# npx @appsoftwareltd/etherpk-mcp login --server https://server.etherpk.com --recovery-code
|
|
23
32
|
|
|
24
33
|
# Tell the agent about the graph (Claude Code shown; the Agents tab has the others).
|
|
25
34
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <graph id>
|
|
26
35
|
```
|
|
27
36
|
|
|
28
37
|
`npx @appsoftwareltd/etherpk-mcp graphs` lists the graphs the token can reach, by name and id.
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the prompts.
|
|
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.
|
|
31
43
|
|
|
32
44
|
## What the agent gets
|
|
33
45
|
|
|
@@ -36,11 +48,43 @@ unique old→new replacement that merges with anyone typing elsewhere on the pag
|
|
|
36
48
|
`append_document` (a page, or a day's journal entry, created if needed) and `create_page`.
|
|
37
49
|
One running instance serves one graph.
|
|
38
50
|
|
|
39
|
-
##
|
|
51
|
+
## What it keeps on your computer
|
|
52
|
+
|
|
53
|
+
- **Keys**: `~/.config/etherpk/mcp.json`, readable only by your user - the same trust as a browser
|
|
54
|
+
you've signed in on.
|
|
55
|
+
- **A cache of each graph you serve**: `~/.cache/etherpk/mcp/`, so a restart catches up on what
|
|
56
|
+
changed instead of downloading everything again. It holds your notes readably, like a signed-in
|
|
57
|
+
browser's own storage does. It is never the source of truth: on every start the Sync Server is
|
|
58
|
+
asked what moved and only that is fetched, edits made elsewhere arrive as they happen, and a
|
|
59
|
+
newer version of this program discards a cache it no longer understands and rebuilds it.
|
|
40
60
|
|
|
41
61
|
Revoke the token at the portal to cut the agent off, and `npx @appsoftwareltd/etherpk-mcp logout`
|
|
42
|
-
to forget the
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
to forget the keys and delete the cache on that computer.
|
|
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-server/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`.
|
|
45
88
|
|
|
46
|
-
Full guide:
|
|
89
|
+
Full guide, including how the cache stays current and what an agent can and can't do:
|
|
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.1.2",
|
|
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.
|
|
@@ -618,7 +673,8 @@ async function connectAccount(config) {
|
|
|
618
673
|
id: me.principal.id,
|
|
619
674
|
email: me.principal.email,
|
|
620
675
|
name: me.principal.name
|
|
621
|
-
}
|
|
676
|
+
},
|
|
677
|
+
clientUrl: me.clientUrl ?? null
|
|
622
678
|
};
|
|
623
679
|
}
|
|
624
680
|
/**
|
|
@@ -858,6 +914,7 @@ z.strictObject({
|
|
|
858
914
|
image: httpsUrl.nullable()
|
|
859
915
|
}),
|
|
860
916
|
authentication: syncAuthenticationSchema,
|
|
917
|
+
clientUrl: httpsUrl.optional(),
|
|
861
918
|
entitlement: z.strictObject({
|
|
862
919
|
plan: z.string().min(1).max(64),
|
|
863
920
|
status: serviceEntitlementSchema.shape.status,
|
|
@@ -1976,37 +2033,50 @@ function serializeClientMessage(message) {
|
|
|
1976
2033
|
* still reads on a white surface, and light enough that dark label text works on all of them
|
|
1977
2034
|
* in both themes. The selection tint is the same colour at 40% alpha (`66`) — the old 20%
|
|
1978
2035
|
* tint of a primary was already faint; a pastel at 20% disappears on white.
|
|
2036
|
+
*
|
|
2037
|
+
* The same eight are offered as the ready-made toolbar colours in Graph Settings (ADR 0071):
|
|
2038
|
+
* one palette, so a graph's bar and a member's caret speak the same language. The label is
|
|
2039
|
+
* for a swatch there; presence itself never shows it (`label`, not `name`: an entry is
|
|
2040
|
+
* spread into a {@link PresenceIdentity}, whose `name` is the pseudonym).
|
|
1979
2041
|
*/
|
|
1980
2042
|
var PRESENCE_PALETTE = [
|
|
1981
2043
|
{
|
|
2044
|
+
label: "Sky",
|
|
1982
2045
|
color: "#7dd3fc",
|
|
1983
2046
|
colorLight: "#7dd3fc66"
|
|
1984
2047
|
},
|
|
1985
2048
|
{
|
|
2049
|
+
label: "Amber",
|
|
1986
2050
|
color: "#fcd34d",
|
|
1987
2051
|
colorLight: "#fcd34d66"
|
|
1988
2052
|
},
|
|
1989
2053
|
{
|
|
2054
|
+
label: "Mint",
|
|
1990
2055
|
color: "#6ee7b7",
|
|
1991
2056
|
colorLight: "#6ee7b766"
|
|
1992
2057
|
},
|
|
1993
2058
|
{
|
|
2059
|
+
label: "Salmon",
|
|
1994
2060
|
color: "#fca5a5",
|
|
1995
2061
|
colorLight: "#fca5a566"
|
|
1996
2062
|
},
|
|
1997
2063
|
{
|
|
2064
|
+
label: "Lavender",
|
|
1998
2065
|
color: "#c4b5fd",
|
|
1999
2066
|
colorLight: "#c4b5fd66"
|
|
2000
2067
|
},
|
|
2001
2068
|
{
|
|
2069
|
+
label: "Pink",
|
|
2002
2070
|
color: "#f9a8d4",
|
|
2003
2071
|
colorLight: "#f9a8d466"
|
|
2004
2072
|
},
|
|
2005
2073
|
{
|
|
2074
|
+
label: "Peach",
|
|
2006
2075
|
color: "#fdba74",
|
|
2007
2076
|
colorLight: "#fdba7466"
|
|
2008
2077
|
},
|
|
2009
2078
|
{
|
|
2079
|
+
label: "Lime",
|
|
2010
2080
|
color: "#bef264",
|
|
2011
2081
|
colorLight: "#bef26466"
|
|
2012
2082
|
}
|
|
@@ -7921,25 +7991,39 @@ async function pollDeviceApproval(api, request) {
|
|
|
7921
7991
|
*
|
|
7922
7992
|
* Pure over an injected clock and output so the flow is testable without a terminal.
|
|
7923
7993
|
*/
|
|
7994
|
+
/** The user left the approval wait on purpose (the signal fired); nothing went wrong. */
|
|
7995
|
+
var ApprovalAbandoned = class extends Error {
|
|
7996
|
+
constructor() {
|
|
7997
|
+
super("Approval abandoned.");
|
|
7998
|
+
this.name = "ApprovalAbandoned";
|
|
7999
|
+
}
|
|
8000
|
+
};
|
|
7924
8001
|
/** Approval rows expire server-side after ten minutes; poll a little longer and then give up. */
|
|
7925
8002
|
var APPROVAL_TIMEOUT_MS = 11 * 6e4;
|
|
7926
8003
|
var APPROVAL_POLL_MS = 2e3;
|
|
7927
8004
|
async function unlockByDeviceApproval(api, io) {
|
|
7928
8005
|
const request = await beginDeviceApproval(api);
|
|
8006
|
+
const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : "open EtherPK in a browser";
|
|
7929
8007
|
io.say("");
|
|
7930
|
-
io.say(`
|
|
8008
|
+
io.say(`To approve this device, ${where} (any page - it need not be a note) signed in to this account`);
|
|
8009
|
+
io.say("with its graphs unlocked. A prompt will show a code; confirm it matches this one:");
|
|
7931
8010
|
io.say("");
|
|
7932
8011
|
io.say(` ${request.sas}`);
|
|
7933
8012
|
io.say("");
|
|
7934
8013
|
io.say("Waiting (up to ten minutes)…");
|
|
8014
|
+
const abandon = async () => {
|
|
8015
|
+
await api.cancelDeviceApproval(request.id).catch(() => {});
|
|
8016
|
+
throw new ApprovalAbandoned();
|
|
8017
|
+
};
|
|
7935
8018
|
const deadline = Date.now() + APPROVAL_TIMEOUT_MS;
|
|
7936
8019
|
while (Date.now() < deadline) {
|
|
7937
|
-
if (io.signal?.aborted)
|
|
8020
|
+
if (io.signal?.aborted) await abandon();
|
|
7938
8021
|
const outcome = await pollDeviceApproval(api, request);
|
|
7939
8022
|
if (outcome.state === "unlocked") return outcome.deviceKey;
|
|
7940
8023
|
if (outcome.state === "rejected") throw new Error("The approval was rejected in EtherPK.");
|
|
7941
8024
|
if (outcome.state === "expired") throw new Error("The approval expired before it was confirmed. Run login again.");
|
|
7942
8025
|
await io.sleep(APPROVAL_POLL_MS);
|
|
8026
|
+
if (io.signal?.aborted) await abandon();
|
|
7943
8027
|
}
|
|
7944
8028
|
throw new Error("The approval was not confirmed in time. Run login again.");
|
|
7945
8029
|
}
|
|
@@ -8314,21 +8398,29 @@ function createMcpServer(graph, info) {
|
|
|
8314
8398
|
* `serve` speaks MCP over stdio, so everything for the human goes to stderr; stdout belongs
|
|
8315
8399
|
* to the agent. `login` and `graphs` are interactive and print to stdout.
|
|
8316
8400
|
*/
|
|
8317
|
-
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";
|
|
8318
8409
|
var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synced graph)
|
|
8319
8410
|
|
|
8320
|
-
|
|
8411
|
+
${CMD} login --server <url> [--pat <token>] [--recovery-code]
|
|
8321
8412
|
Sign this machine in as a device of your account. Prompts for a Personal Access
|
|
8322
8413
|
Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)
|
|
8323
|
-
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8414
|
+
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:
|
|
8415
|
+
open EtherPK in a browser signed in to the account with its graphs unlocked and
|
|
8416
|
+
confirm the code shown. Press r while waiting, or pass --recovery-code, to type
|
|
8417
|
+
your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).
|
|
8418
|
+
${CMD} graphs
|
|
8327
8419
|
List the synced graphs this account can reach, by name and id.
|
|
8328
|
-
|
|
8420
|
+
${CMD} serve --graph <id or name>
|
|
8329
8421
|
Serve one graph to an agent over stdio. For Claude Code:
|
|
8330
8422
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <id>
|
|
8331
|
-
|
|
8423
|
+
${CMD} logout
|
|
8332
8424
|
Forget the token, keys and cached graphs on this machine.
|
|
8333
8425
|
|
|
8334
8426
|
The config file is ${defaultConfigPath()} (override with ETHERPK_MCP_CONFIG); cached graphs live
|
|
@@ -8379,7 +8471,7 @@ async function ask(question, { secret = false } = {}) {
|
|
|
8379
8471
|
}
|
|
8380
8472
|
async function requireConfig(path) {
|
|
8381
8473
|
const config = await readConfig(path);
|
|
8382
|
-
if (!config) fail(`Not logged in on this machine. Run:
|
|
8474
|
+
if (!config) fail(`Not logged in on this machine. Run: ${CMD} login --server <url>`);
|
|
8383
8475
|
return config;
|
|
8384
8476
|
}
|
|
8385
8477
|
async function login(args) {
|
|
@@ -8393,10 +8485,8 @@ async function login(args) {
|
|
|
8393
8485
|
pat
|
|
8394
8486
|
});
|
|
8395
8487
|
console.log(`Signed in to ${server} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`);
|
|
8396
|
-
const
|
|
8397
|
-
|
|
8398
|
-
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
8399
|
-
});
|
|
8488
|
+
const byRecoveryCode = async () => unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? await ask("Recovery Code: ", { secret: true }));
|
|
8489
|
+
const vaultKey = args["recovery-code"] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode);
|
|
8400
8490
|
await writeConfig(path, {
|
|
8401
8491
|
server,
|
|
8402
8492
|
pat,
|
|
@@ -8409,8 +8499,55 @@ async function login(args) {
|
|
|
8409
8499
|
vaultKey: toBase64Url(vaultKey)
|
|
8410
8500
|
});
|
|
8411
8501
|
}
|
|
8502
|
+
/**
|
|
8503
|
+
* Device Approval, with the Recovery Code one keypress away: a user who has no unlocked EtherPK
|
|
8504
|
+
* to hand should not have to Ctrl-C and re-read the help to find `--recovery-code`. On a
|
|
8505
|
+
* terminal, `r` during the wait abandons the approval (cancelled server-side) and asks for the
|
|
8506
|
+
* code instead; without a terminal the wait runs to its outcome.
|
|
8507
|
+
*/
|
|
8508
|
+
async function approveOrFallBack(account, byRecoveryCode) {
|
|
8509
|
+
const abort = new AbortController();
|
|
8510
|
+
const io = {
|
|
8511
|
+
say: (line) => console.log(line),
|
|
8512
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
8513
|
+
signal: abort.signal,
|
|
8514
|
+
clientUrl: account.clientUrl
|
|
8515
|
+
};
|
|
8516
|
+
const stdin = process.stdin;
|
|
8517
|
+
const interactive = stdin.isTTY === true;
|
|
8518
|
+
const onKey = (chunk) => {
|
|
8519
|
+
const key = chunk.toString("utf8");
|
|
8520
|
+
if (key === "r" || key === "R") abort.abort();
|
|
8521
|
+
if (key === "") {
|
|
8522
|
+
console.log("");
|
|
8523
|
+
process.exit(130);
|
|
8524
|
+
}
|
|
8525
|
+
};
|
|
8526
|
+
if (interactive) {
|
|
8527
|
+
console.log("(Press r to type your Recovery Code instead.)");
|
|
8528
|
+
stdin.setRawMode(true);
|
|
8529
|
+
stdin.resume();
|
|
8530
|
+
stdin.on("data", onKey);
|
|
8531
|
+
}
|
|
8532
|
+
let abandoned = false;
|
|
8533
|
+
try {
|
|
8534
|
+
return await unlockByDeviceApproval(account.api, io);
|
|
8535
|
+
} catch (error) {
|
|
8536
|
+
if (!(error instanceof ApprovalAbandoned)) throw error;
|
|
8537
|
+
abandoned = true;
|
|
8538
|
+
} finally {
|
|
8539
|
+
if (interactive) {
|
|
8540
|
+
stdin.off("data", onKey);
|
|
8541
|
+
stdin.setRawMode(false);
|
|
8542
|
+
stdin.pause();
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8545
|
+
if (!abandoned) throw new Error("unreachable");
|
|
8546
|
+
console.log("Approval cancelled; unlocking with your Recovery Code instead.");
|
|
8547
|
+
return byRecoveryCode();
|
|
8548
|
+
}
|
|
8412
8549
|
async function listGraphs(config) {
|
|
8413
|
-
if (!config.vaultKey) fail(
|
|
8550
|
+
if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
|
|
8414
8551
|
const account = await connectAccount(config);
|
|
8415
8552
|
const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
|
|
8416
8553
|
const graphs = await account.api.listGraphs();
|
|
@@ -8432,13 +8569,14 @@ async function listGraphs(config) {
|
|
|
8432
8569
|
console.log(` ${record.id} ${label} [${record.role}]`);
|
|
8433
8570
|
}
|
|
8434
8571
|
console.log("");
|
|
8435
|
-
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>`);
|
|
8436
8574
|
}
|
|
8437
8575
|
async function serve(args) {
|
|
8438
8576
|
const wanted = args.graph?.trim();
|
|
8439
8577
|
if (!wanted) fail("serve needs --graph <id or name>.");
|
|
8440
8578
|
const config = await requireConfig(defaultConfigPath());
|
|
8441
|
-
if (!config.vaultKey) fail(
|
|
8579
|
+
if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
|
|
8442
8580
|
const account = await connectAccount(config);
|
|
8443
8581
|
const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
|
|
8444
8582
|
const graphs = await account.api.listGraphs();
|
|
@@ -8460,7 +8598,7 @@ async function serve(args) {
|
|
|
8460
8598
|
break;
|
|
8461
8599
|
}
|
|
8462
8600
|
}
|
|
8463
|
-
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`);
|
|
8464
8602
|
const { record, keyring } = resolveGraphById(graphs, vault, graphId);
|
|
8465
8603
|
console.error(`etherpk-mcp: opening graph ${graphId} on ${account.serverBaseUrl}…`);
|
|
8466
8604
|
const graph = await openHeadlessGraph({
|