@openvole/volenet-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenVole
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,138 @@
1
+ # @openvole/volenet-mcp
2
+
3
+ VoleNet as an MCP server — an identity on the mesh for Claude Code, so a coding session can reach
4
+ people and other agents across machines it does not own.
5
+
6
+ Claude Code is very good inside one machine and one session. It cannot message a person on their
7
+ phone, cannot talk to an agent someone else runs, and has no identity that outlives the session.
8
+ None of that is coding-assistant work, and none of it is what a subagent solves: a subagent is the
9
+ same principal on the same machine. This is the other thing — signed identity, hybrid post-quantum
10
+ sealing, a hub that carries ciphertext it cannot read, consent before anyone can message you, and
11
+ hold-and-forward for a peer that is not there right now.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npx -y @openvole/volenet-mcp install
17
+ ```
18
+
19
+ That is the whole setup. It registers the server with Claude Code, and there is nothing to
20
+ configure: an identity is generated on first run, the name defaults to `claude-<hostname>`, and
21
+ whether to join a hub is a decision you make later, from inside a session.
22
+
23
+ Restart Claude Code afterwards — MCP servers load at startup.
24
+
25
+ Everything after that happens **in conversation, not in a shell**: these are tools Claude calls on
26
+ your behalf, not commands you type. Ask for them in your own words.
27
+
28
+ > *"who am I on the mesh?"* → `volenet_whoami`
29
+ >
30
+ > *"join the hub at https://hub.example.com/mesh"* → `volenet_hub`, and it is remembered
31
+ >
32
+ > *"pair with the agent at http://10.0.0.5:9700"* → `volenet_connect`, which reports the
33
+ > fingerprint first and pairs once you confirm it
34
+
35
+ Add `--user` to register it in every project rather than the current one. If the `claude` CLI is
36
+ not on PATH, the installer prints the one line to paste instead of guessing at its config.
37
+
38
+ ### Settings
39
+
40
+ Settings live with the identity in `~/.openvole/volenet-mcp/`, not in the command that launches
41
+ the server, so changing one never means re-registering anything. `volenet_hub` writes the hub
42
+ there; everything else has a default worth keeping.
43
+
44
+ | variable | default | what |
45
+ |---|---|---|
46
+ | `VOLENET_MCP_NAME` | `claude-<hostname>` | What peers see. Not identity — the key is. |
47
+ | `VOLENET_MCP_HUB` | none | Normally set by `volenet_hub`; this overrides it for scripted setups. |
48
+ | `VOLENET_MCP_DIR` | `~/.openvole/volenet-mcp` | Keypair, trust store, settings, transcript. |
49
+ | `VOLENET_MCP_PORT` | `9750` | Listening port, for peers that *can* dial you. |
50
+
51
+ Environment wins over stored settings, which win over defaults. None of it is required.
52
+
53
+ That directory **is** your identity: back it up, and anyone who has it is you.
54
+
55
+ ## Commands
56
+
57
+ Alongside the tools, a few things are useful before a session exists, or without one. These touch
58
+ files only — no node is started, no port is bound — so they are safe to run while the server is up,
59
+ and safe in a hook that fires on every session.
60
+
61
+ ```bash
62
+ volenet-mcp install [--user] register with Claude Code
63
+ volenet-mcp whoami this machine's identity on the mesh
64
+ volenet-mcp hub <url> set the hub; joined on the next session start
65
+ volenet-mcp hub --leave come off it
66
+ volenet-mcp inbox [--read] what is waiting
67
+ ```
68
+
69
+ Anything needing a live node — the roster, pairing, asking an agent's brain — is a tool rather
70
+ than a command, because it needs a running node and a conversation to happen in.
71
+
72
+ ## Tools
73
+
74
+ | tool | what it does |
75
+ |---|---|
76
+ | `volenet_whoami` | This session's identity, hub and reachability. |
77
+ | `volenet_peers` | Everyone reachable, direct or via a hub, online or away. |
78
+ | `volenet_inbox` | What arrived, including while the session was not running. |
79
+ | `volenet_send` | Message a person or agent. Chat — it does not run their brain. |
80
+ | `volenet_history` | The thread with one peer. |
81
+ | `volenet_ask` | Ask another **agent's** brain a question and wait for the answer. |
82
+ | `volenet_requests` | Trust decisions waiting on you; accept or deny. |
83
+ | `volenet_wait` | Wait for the next message instead of checking again later. |
84
+ | `volenet_hub` | Join a hub, leave one, or say which you are on. Remembered. |
85
+ | `volenet_connect` | Pair with a node, or ask a hub member for consent to chat. |
86
+
87
+ ## What it does not do
88
+
89
+ - **It cannot make an unreachable peer reachable.** Direct links need a dialable address; behind
90
+ carrier-grade NAT that is nobody's phone. That is what the hub is for.
91
+ - **A hub will not carry `volenet_ask`.** The relay allowlist is chat, consent and file control —
92
+ nothing executable travels through a third party. Asking an agent's brain needs a direct link.
93
+ - **Nothing is trusted because it connected.** A peer must be paired, or consented to through a
94
+ hub, before it can say anything to you. Pairing by URL is two calls: the first reports the
95
+ fingerprint of whoever answered, the second confirms it.
96
+ - **No message is stored on a hub.** Envelopes are sealed to static keys with no ratchet, so
97
+ ciphertext at rest would be retroactively readable if a key ever leaked. An undelivered message
98
+ waits on the sender; the hub keeps a notice — who tried, how often, when — and nothing else.
99
+
100
+ ## Being told, when nothing can tell you
101
+
102
+ MCP is pull-only: a server cannot wake its client or push into a conversation. A message that
103
+ arrives is written to the inbox immediately and is never lost, but nothing announces it. Two
104
+ things close most of that gap:
105
+
106
+ - **Every tool result says what is unread** — `— 2 unread messages from X` — so any use of any
107
+ tool surfaces it, without being asked.
108
+ - **`volenet_wait` blocks until something arrives**, rather than returning nothing and being
109
+ called again. That is what makes a back-and-forth feel like a conversation instead of a
110
+ mailbox: say something, wait, get the reply in the same turn.
111
+
112
+ For catch-up at the start of a session, ask for the inbox — or have it arrive before you type
113
+ anything, with a `SessionStart` hook in `.claude/settings.json`:
114
+
115
+ ```json
116
+ {
117
+ "hooks": {
118
+ "SessionStart": [
119
+ { "hooks": [{ "type": "command", "command": "npx -y @openvole/volenet-mcp inbox --read" }] }
120
+ ]
121
+ }
122
+ }
123
+ ```
124
+
125
+ `--read` marks them seen, since the hook has just put them in front of you.
126
+
127
+ ## Session lifetime
128
+
129
+ The node lives as long as the editor session. That is a supported shape, not a degraded one: a
130
+ sender holds what it could not deliver and flushes when you reappear, and a hub hands you notices
131
+ about who tried while you were gone. `volenet_inbox` is the first thing to call in a new session.
132
+
133
+ ## Build and test
134
+
135
+ ```bash
136
+ pnpm -C src/volenet-mcp build
137
+ pnpm -C src/volenet-mcp test
138
+ ```
@@ -0,0 +1,137 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { VoleNetManager } from '@openvole/volenet';
3
+
4
+ interface StoredConfig {
5
+ name?: string;
6
+ hub?: string;
7
+ port?: number;
8
+ }
9
+ interface Settings {
10
+ name: string;
11
+ hub?: string;
12
+ dir: string;
13
+ port: number;
14
+ }
15
+ declare function defaultDir(): string;
16
+ /** A name that says what this is without needing to be chosen. Identity is the key, not this. */
17
+ declare function defaultName(): string;
18
+ declare function loadStored(dir: string): Promise<StoredConfig>;
19
+ declare function saveStored(dir: string, patch: StoredConfig): Promise<StoredConfig>;
20
+ /** Environment over stored settings over defaults. Every layer is optional. */
21
+ declare function resolveSettings(): Promise<Settings>;
22
+
23
+ interface Message {
24
+ /** The peer this is with — the sender for 'in', the recipient for 'out'. */
25
+ peerId: string;
26
+ peerName: string;
27
+ dir: 'in' | 'out';
28
+ text: string;
29
+ /** Milliseconds since the epoch, from the signed message for 'in'. */
30
+ ts: number;
31
+ /** The signed message id, so a replay or a double-flush cannot duplicate a line. */
32
+ id: string;
33
+ }
34
+ declare class Inbox {
35
+ private readonly file;
36
+ private messages;
37
+ /** Per peer, the timestamp up to which the session has been shown its messages. */
38
+ private readAt;
39
+ private writing;
40
+ constructor(file: string);
41
+ load(): Promise<void>;
42
+ /** Record a message. Returns false when this id was already recorded. */
43
+ add(m: Message): Promise<boolean>;
44
+ /** Everything with one peer, oldest first. */
45
+ history(peerId: string, limit?: number): Message[];
46
+ /** Inbound messages the session has not been shown yet, oldest first. */
47
+ unread(): Message[];
48
+ /** Mark everything currently unread as seen. */
49
+ markRead(): Promise<void>;
50
+ /** Every peer we have said anything to or heard anything from, most recent first. */
51
+ peers(): Array<{
52
+ peerId: string;
53
+ peerName: string;
54
+ last: number;
55
+ unread: number;
56
+ }>;
57
+ get size(): number;
58
+ /** Serialised: two messages arriving together must not race each other's rewrite. */
59
+ private persist;
60
+ }
61
+
62
+ /** What a node needs to start. Resolved from stored settings, env and defaults. */
63
+ type NodeOptions = Settings;
64
+
65
+ interface PendingRequest {
66
+ kind: 'pair' | 'relay';
67
+ from: string;
68
+ fromName: string;
69
+ note?: string;
70
+ at: number;
71
+ }
72
+ interface Notice {
73
+ from: string;
74
+ fromName: string;
75
+ count: number;
76
+ last: number;
77
+ }
78
+ interface Node {
79
+ net: VoleNetManager;
80
+ inbox: Inbox;
81
+ /** What happened when we tried to join the configured hub, for whoami to report honestly. */
82
+ hubStatus: string;
83
+ /** Trust decisions waiting on the person, newest last. */
84
+ requests: PendingRequest[];
85
+ /** Who tried to reach us while we were away, as the hub reports on reconnect. */
86
+ notices: Notice[];
87
+ options: NodeOptions;
88
+ /**
89
+ * Be told when a message lands. MCP cannot push, so a session that wants to *wait* for a reply
90
+ * — rather than poll for one — needs somewhere to hang a promise. Returns an unsubscribe.
91
+ */
92
+ onMessage: (fn: (m: Message) => void) => () => void;
93
+ stop: () => Promise<void>;
94
+ }
95
+ declare function startNode(options: NodeOptions): Promise<Node>;
96
+
97
+ declare function run(argv: string[], out?: NodeJS.WriteStream & {
98
+ fd: 1;
99
+ }): Promise<number>;
100
+
101
+ interface ToolDef {
102
+ name: string;
103
+ description: string;
104
+ inputSchema: Record<string, unknown>;
105
+ run: (node: Node, args: Record<string, unknown>) => Promise<string>;
106
+ }
107
+ declare const TOOLS: ToolDef[];
108
+
109
+ /**
110
+ * VoleNet as an MCP server.
111
+ *
112
+ * Claude Code is very good inside one machine and one session. It has no way to reach a person on
113
+ * their phone, no way to talk to an agent someone else owns, and no identity that outlives the
114
+ * session. VoleNet has all three and none of it is coding-assistant work — signed identity, hybrid
115
+ * post-quantum sealing, a hub that carries ciphertext it cannot read, consent, and hold-and-forward
116
+ * for a peer that is not there right now.
117
+ *
118
+ * So this is not another agent. It is the network, handed to an agent that already exists.
119
+ *
120
+ * stdio transport, which means **stdout belongs to the protocol**: anything printed there that is
121
+ * not a JSON-RPC frame breaks the client. The core logger is silent by default and writes to a file
122
+ * when VOLE_LOG_FILE is set; diagnostics here go to stderr.
123
+ */
124
+
125
+ /**
126
+ * What is waiting, appended to every tool's result.
127
+ *
128
+ * MCP has no way for a server to push, so an arrived message would otherwise sit unseen until
129
+ * somebody thought to look. Saying so on every result means any use of any tool surfaces it —
130
+ * ambient awareness in place of the notification the protocol cannot send. The tools that just
131
+ * showed you the messages are excluded, since they leave nothing unread.
132
+ */
133
+ declare function unreadFooter(node: Node, toolName: string): string;
134
+ /** Wire the tools to an MCP server. Separated so a test can drive it without a transport. */
135
+ declare function createServer(node: Node): Server;
136
+
137
+ export { Inbox, type Node, type NodeOptions, type Settings, TOOLS, type ToolDef, createServer, defaultDir, defaultName, loadStored, resolveSettings, run as runCli, saveStored, startNode, unreadFooter };
package/dist/index.js ADDED
@@ -0,0 +1,816 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ // src/cli.ts
9
+ import * as path4 from "path";
10
+ import { loadKeyPair } from "@openvole/volenet";
11
+
12
+ // src/config.ts
13
+ import * as fs from "fs/promises";
14
+ import * as os from "os";
15
+ import * as path from "path";
16
+ function defaultDir() {
17
+ return process.env.VOLENET_MCP_DIR?.trim() || path.join(os.homedir(), ".openvole", "volenet-mcp");
18
+ }
19
+ function defaultName() {
20
+ return `claude-${os.hostname().split(".")[0].toLowerCase()}`;
21
+ }
22
+ var file = (dir) => path.join(dir, "config.json");
23
+ async function loadStored(dir) {
24
+ try {
25
+ const raw = JSON.parse(await fs.readFile(file(dir), "utf-8"));
26
+ return raw && typeof raw === "object" ? raw : {};
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+ async function saveStored(dir, patch) {
32
+ const next = { ...await loadStored(dir), ...patch };
33
+ for (const k of Object.keys(next)) {
34
+ if (next[k] === void 0) delete next[k];
35
+ }
36
+ await fs.mkdir(dir, { recursive: true });
37
+ await fs.writeFile(file(dir), `${JSON.stringify(next, null, 2)}
38
+ `, "utf-8");
39
+ return next;
40
+ }
41
+ async function resolveSettings() {
42
+ const dir = defaultDir();
43
+ const stored = await loadStored(dir);
44
+ const envPort = Number(process.env.VOLENET_MCP_PORT);
45
+ return {
46
+ name: process.env.VOLENET_MCP_NAME?.trim() || stored.name || defaultName(),
47
+ hub: process.env.VOLENET_MCP_HUB?.trim() || stored.hub || void 0,
48
+ dir,
49
+ port: (Number.isFinite(envPort) && envPort > 0 ? envPort : stored.port) || 9750
50
+ };
51
+ }
52
+
53
+ // src/inbox.ts
54
+ import * as fs2 from "fs/promises";
55
+ import * as path2 from "path";
56
+ var MAX_MESSAGES = 2e3;
57
+ var Inbox = class {
58
+ constructor(file2) {
59
+ this.file = file2;
60
+ }
61
+ file;
62
+ messages = [];
63
+ /** Per peer, the timestamp up to which the session has been shown its messages. */
64
+ readAt = /* @__PURE__ */ new Map();
65
+ writing = Promise.resolve();
66
+ async load() {
67
+ try {
68
+ const raw = JSON.parse(await fs2.readFile(this.file, "utf-8"));
69
+ this.messages = (raw.messages ?? []).filter(
70
+ (m) => m && typeof m.peerId === "string" && typeof m.text === "string"
71
+ );
72
+ this.readAt = new Map(Object.entries(raw.readAt ?? {}));
73
+ } catch {
74
+ this.messages = [];
75
+ this.readAt = /* @__PURE__ */ new Map();
76
+ }
77
+ }
78
+ /** Record a message. Returns false when this id was already recorded. */
79
+ async add(m) {
80
+ if (this.messages.some((x) => x.id === m.id)) return false;
81
+ this.messages.push(m);
82
+ if (this.messages.length > MAX_MESSAGES) {
83
+ this.messages.splice(0, this.messages.length - MAX_MESSAGES);
84
+ }
85
+ await this.persist();
86
+ return true;
87
+ }
88
+ /** Everything with one peer, oldest first. */
89
+ history(peerId, limit = 50) {
90
+ return this.messages.filter((m) => m.peerId === peerId).slice(-limit);
91
+ }
92
+ /** Inbound messages the session has not been shown yet, oldest first. */
93
+ unread() {
94
+ return this.messages.filter((m) => m.dir === "in" && m.ts > (this.readAt.get(m.peerId) ?? 0));
95
+ }
96
+ /** Mark everything currently unread as seen. */
97
+ async markRead() {
98
+ for (const m of this.unread()) {
99
+ const at = this.readAt.get(m.peerId) ?? 0;
100
+ if (m.ts > at) this.readAt.set(m.peerId, m.ts);
101
+ }
102
+ await this.persist();
103
+ }
104
+ /** Every peer we have said anything to or heard anything from, most recent first. */
105
+ peers() {
106
+ const by = /* @__PURE__ */ new Map();
107
+ for (const m of this.messages) {
108
+ const e = by.get(m.peerId) ?? { peerId: m.peerId, peerName: m.peerName, last: 0, unread: 0 };
109
+ if (m.peerName) e.peerName = m.peerName;
110
+ e.last = Math.max(e.last, m.ts);
111
+ if (m.dir === "in" && m.ts > (this.readAt.get(m.peerId) ?? 0)) e.unread++;
112
+ by.set(m.peerId, e);
113
+ }
114
+ return [...by.values()].sort((a, b) => b.last - a.last);
115
+ }
116
+ get size() {
117
+ return this.messages.length;
118
+ }
119
+ /** Serialised: two messages arriving together must not race each other's rewrite. */
120
+ persist() {
121
+ this.writing = this.writing.then(async () => {
122
+ const body = JSON.stringify(
123
+ { messages: this.messages, readAt: Object.fromEntries(this.readAt) },
124
+ null,
125
+ 2
126
+ );
127
+ await fs2.mkdir(path2.dirname(this.file), { recursive: true });
128
+ const tmp = `${this.file}.tmp`;
129
+ await fs2.writeFile(tmp, body, "utf-8");
130
+ await fs2.rename(tmp, this.file);
131
+ });
132
+ return this.writing;
133
+ }
134
+ };
135
+
136
+ // src/install.ts
137
+ import { spawnSync } from "child_process";
138
+ import * as path3 from "path";
139
+ var SERVER_NAME = "volenet";
140
+ var PACKAGE = "@openvole/volenet-mcp";
141
+ function launchCommand(entry = process.argv[1]) {
142
+ const installed = !entry || !entry.endsWith(".js") || entry.includes(`${path3.sep}node_modules${path3.sep}`);
143
+ return installed ? ["npx", "-y", PACKAGE] : ["node", entry];
144
+ }
145
+ function addArgs(scope, command) {
146
+ return ["mcp", "add", SERVER_NAME, "-s", scope, "--", ...command];
147
+ }
148
+ var NEXT_STEPS = '\nRestart Claude Code \u2014 MCP servers load at startup \u2014 then:\n\n volenet_whoami who you are on the mesh (an identity is made on first run)\n volenet_hub url:"..." join a hub, to be reachable from anywhere\n volenet_connect url:"..." or pair directly with an agent you can dial\n\nNothing else needs configuring.\n';
149
+ function install(argv, out = process.stdout) {
150
+ const scope = argv.includes("--user") ? "user" : "local";
151
+ const command = launchCommand();
152
+ const paste = `claude mcp add ${SERVER_NAME} -s ${scope} -- ${command.join(" ")}`;
153
+ const run2 = (args) => spawnSync("claude", args, {
154
+ stdio: ["ignore", "pipe", "pipe"],
155
+ encoding: "utf-8",
156
+ timeout: 3e4
157
+ });
158
+ const listed = run2(["mcp", "list"]);
159
+ if (listed.error) {
160
+ out.write(
161
+ `The \`claude\` CLI is not on PATH. Run this once, in the project you want it in:
162
+
163
+ ${paste}
164
+
165
+ `
166
+ );
167
+ return 1;
168
+ }
169
+ if (listed.stdout?.includes(`${SERVER_NAME}:`)) {
170
+ out.write(`${SERVER_NAME} is already registered \u2014 nothing to do.
171
+ ${NEXT_STEPS}`);
172
+ return 0;
173
+ }
174
+ const added = run2(addArgs(scope, command));
175
+ if (added.status !== 0) {
176
+ out.write(
177
+ `Could not register it automatically${added.stderr ? `: ${added.stderr.trim()}` : ""}
178
+
179
+ Run this once instead:
180
+
181
+ ${paste}
182
+
183
+ `
184
+ );
185
+ return added.status ?? 1;
186
+ }
187
+ out.write(`Registered ${SERVER_NAME} (${scope} scope).
188
+ ${NEXT_STEPS}`);
189
+ return 0;
190
+ }
191
+
192
+ // src/cli.ts
193
+ var USAGE = `volenet-mcp \u2014 VoleNet as an MCP server
194
+
195
+ volenet-mcp install [--user] register with Claude Code (default: this project)
196
+ volenet-mcp whoami this machine's identity on the mesh
197
+ volenet-mcp hub [url|--leave] which hub to use; takes effect on the next session
198
+ volenet-mcp inbox [--read] messages waiting, for catching up or a SessionStart hook
199
+
200
+ With no command it runs as the MCP server itself, over stdio, which is how Claude Code starts it.
201
+ Anything needing a live node \u2014 peers, pairing, asking an agent's brain \u2014 is a tool you ask for in
202
+ a session, not a command here.
203
+ `;
204
+ var when = (ts) => new Date(ts).toISOString().replace("T", " ").slice(0, 16);
205
+ async function run(argv, out = process.stdout) {
206
+ const [command, ...rest] = argv;
207
+ const dir = defaultDir();
208
+ if (!command || command === "help" || command === "--help" || command === "-h") {
209
+ out.write(USAGE);
210
+ return 0;
211
+ }
212
+ if (command === "install") return install(rest, out);
213
+ if (command === "whoami") {
214
+ const stored = await loadStored(dir);
215
+ const keys = await loadKeyPair(path4.join(dir, "net")).catch(() => null);
216
+ if (!keys) {
217
+ out.write(
218
+ `No identity yet at ${dir}.
219
+ One is generated the first time the server runs \u2014 start a session, or ask for volenet_whoami.
220
+ `
221
+ );
222
+ return 0;
223
+ }
224
+ out.write(
225
+ [
226
+ `name ${stored.name ?? defaultName()}`,
227
+ `instanceId ${keys.instanceId}`,
228
+ `hub ${stored.hub ?? "(none \u2014 set one with: volenet-mcp hub <url>)"}`,
229
+ `store ${dir}`,
230
+ "",
231
+ "That directory is your identity. Back it up; anyone who has it is you.",
232
+ ""
233
+ ].join("\n")
234
+ );
235
+ return 0;
236
+ }
237
+ if (command === "hub") {
238
+ const stored = await loadStored(dir);
239
+ if (rest.includes("--leave")) {
240
+ if (!stored.hub) {
241
+ out.write("Not on a hub.\n");
242
+ return 0;
243
+ }
244
+ await saveStored(dir, { hub: void 0 });
245
+ out.write(`Left ${stored.hub}. Your identity and direct pairings are untouched.
246
+ `);
247
+ return 0;
248
+ }
249
+ const url = rest.find((a) => !a.startsWith("-"));
250
+ if (!url) {
251
+ out.write(stored.hub ? `${stored.hub}
252
+ ` : "No hub set. Give one: volenet-mcp hub <url>\n");
253
+ return 0;
254
+ }
255
+ await saveStored(dir, { hub: url.replace(/\/$/, "") });
256
+ out.write(
257
+ `Hub set to ${url}.
258
+ It is joined the next time the server starts \u2014 restart Claude Code, or ask for volenet_hub to do it now.
259
+ `
260
+ );
261
+ return 0;
262
+ }
263
+ if (command === "inbox") {
264
+ const inbox = new Inbox(path4.join(dir, "inbox.json"));
265
+ await inbox.load();
266
+ const unread = inbox.unread();
267
+ if (unread.length === 0) {
268
+ out.write("No new messages.\n");
269
+ return 0;
270
+ }
271
+ out.write(`${unread.length} new VoleNet message${unread.length === 1 ? "" : "s"}:
272
+
273
+ `);
274
+ for (const m of unread) {
275
+ out.write(` [${when(m.ts)}] ${m.peerName}: ${m.text}
276
+ `);
277
+ }
278
+ if (rest.includes("--read")) await inbox.markRead();
279
+ out.write("\n");
280
+ return 0;
281
+ }
282
+ out.write(`Unknown command: ${command}
283
+
284
+ ${USAGE}`);
285
+ return 1;
286
+ }
287
+
288
+ // src/node.ts
289
+ import * as path5 from "path";
290
+ import {
291
+ VoleNetManager,
292
+ createEventBus,
293
+ loadAuthorizedVoles,
294
+ parsePublicKey
295
+ } from "@openvole/volenet";
296
+ async function startNode(options) {
297
+ const bus = createEventBus();
298
+ const inbox = new Inbox(path5.join(options.dir, "inbox.json"));
299
+ await inbox.load();
300
+ const requests = [];
301
+ const notices = [];
302
+ const net = new VoleNetManager(
303
+ {
304
+ enabled: true,
305
+ instanceName: options.name,
306
+ role: "peer",
307
+ port: options.port,
308
+ keyPath: path5.join(options.dir, "net", "vole_key"),
309
+ // A hub is a peer we dial. 'read' rather than 'full': a hub carries our sealed traffic,
310
+ // it has no business acting on this node.
311
+ peers: options.hub ? [{ url: options.hub, trust: "read" }] : []
312
+ },
313
+ options.dir
314
+ );
315
+ const listeners = /* @__PURE__ */ new Set();
316
+ bus.on("volenet:chat", (d) => {
317
+ const m = d;
318
+ const message = {
319
+ peerId: m.from,
320
+ peerName: m.fromName,
321
+ dir: "in",
322
+ text: m.text,
323
+ ts: m.timestamp,
324
+ id: m.messageId
325
+ };
326
+ void inbox.add(message).then((added) => {
327
+ if (added) for (const fn of listeners) fn(message);
328
+ });
329
+ });
330
+ bus.on("volenet:chat:pending", (d) => {
331
+ const p = d;
332
+ for (const n of p.from ?? []) {
333
+ const at = notices.findIndex((x) => x.from === n.from);
334
+ if (at >= 0) notices[at] = n;
335
+ else notices.push(n);
336
+ }
337
+ });
338
+ const remember = (kind) => (d) => {
339
+ const r = d;
340
+ if (requests.some((x) => x.from === r.from && x.kind === kind)) return;
341
+ requests.push({ kind, from: r.from, fromName: r.fromName, note: r.note, at: Date.now() });
342
+ };
343
+ bus.on("volenet:pair:request", remember("pair"));
344
+ bus.on("volenet:relay:request", remember("relay"));
345
+ await net.start(void 0, bus);
346
+ let hubStatus = "no hub configured";
347
+ if (options.hub) {
348
+ hubStatus = await alreadyTrusts(options.dir, options.hub) ? `joined ${options.hub}` : await join4(net, options.hub);
349
+ }
350
+ return {
351
+ net,
352
+ inbox,
353
+ requests,
354
+ notices,
355
+ options,
356
+ hubStatus,
357
+ onMessage: (fn) => {
358
+ listeners.add(fn);
359
+ return () => listeners.delete(fn);
360
+ },
361
+ stop: () => net.stop()
362
+ };
363
+ }
364
+ async function join4(net, hub) {
365
+ const res = await net.initiateJoin(hub);
366
+ if (!res.ok) return `could not join ${hub}: ${res.error}`;
367
+ if (res.pending) return `waiting for approval at ${hub}`;
368
+ return `joined ${res.hubName ?? hub}`;
369
+ }
370
+ async function alreadyTrusts(dir, hub) {
371
+ try {
372
+ const r = await fetch(`${hub.replace(/\/$/, "")}/volenet/info`, {
373
+ signal: AbortSignal.timeout(8e3)
374
+ });
375
+ const info = await r.json();
376
+ const parsed = info.publicKey ? parsePublicKey(info.publicKey) : null;
377
+ if (!parsed) return false;
378
+ return (await loadAuthorizedVoles(path5.join(dir, "net"))).has(parsed.instanceId);
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+
384
+ // src/tools.ts
385
+ var obj = (properties, required = []) => ({
386
+ type: "object",
387
+ properties,
388
+ ...required.length ? { required } : {}
389
+ });
390
+ var str = (description) => ({ type: "string", description });
391
+ var num = (description) => ({ type: "number", description });
392
+ var when2 = (ts) => ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : "-";
393
+ function peers(node) {
394
+ const live = new Set(
395
+ (node.net.getTransport()?.getPeers() ?? []).filter((p) => p.connected).map((p) => p.peerId)
396
+ );
397
+ const out = node.net.getInstances().map((i) => ({
398
+ id: i.id,
399
+ name: i.name,
400
+ route: "direct",
401
+ connected: live.has(i.id)
402
+ }));
403
+ const direct = new Set(out.map((p) => p.id));
404
+ for (const m of node.net.getRelayMembers()) {
405
+ if (direct.has(m.id)) continue;
406
+ out.push({
407
+ id: m.id,
408
+ name: m.name,
409
+ route: "hub",
410
+ connected: m.connected,
411
+ consented: m.accepted,
412
+ viaHub: m.viaHubName
413
+ });
414
+ }
415
+ return out;
416
+ }
417
+ function resolve(node, ref) {
418
+ const all = peers(node);
419
+ return all.find((p) => p.id === ref) ?? all.find((p) => p.name === ref) ?? all.find((p) => p.id.startsWith(ref)) ?? all.find((p) => p.name.toLowerCase() === ref.toLowerCase());
420
+ }
421
+ var TOOLS = [
422
+ {
423
+ name: "volenet_whoami",
424
+ description: "This session's own identity on the VoleNet mesh \u2014 name, instance id, hub, and whether the mesh is reachable. Call this first if you are unsure. Pass key:true only when someone actually needs the full public key; it is several kilobytes of post-quantum key material.",
425
+ inputSchema: obj({
426
+ key: { type: "boolean", description: "Include the full public key string" }
427
+ }),
428
+ async run(node, args) {
429
+ const key = node.net.getKeyPair();
430
+ const online = peers(node).filter((p) => p.connected).length;
431
+ const lines = [
432
+ `name ${node.options.name}`,
433
+ `instanceId ${key?.instanceId ?? "(not started)"}`,
434
+ `hub ${node.hubStatus}`,
435
+ `connected ${online} peer(s) online`,
436
+ `listening port ${node.options.port} (reachable only from networks that can dial it)`,
437
+ `store ${node.options.dir}`
438
+ ];
439
+ if (args.key) lines.push("", `publicKey ${key?.publicKeyString ?? "-"}`);
440
+ else
441
+ lines.push(
442
+ "",
443
+ "Public key withheld (large). Call again with key:true when a peer needs it."
444
+ );
445
+ lines.push(
446
+ "",
447
+ "To be reachable by someone else: give them that public key to trust, or send them a",
448
+ "pair request with volenet_connect and have their operator accept it."
449
+ );
450
+ return lines.join("\n");
451
+ }
452
+ },
453
+ {
454
+ name: "volenet_peers",
455
+ description: "Everyone this session can reach: agents and people, whether the link is direct or through a hub, and whether they are online right now.",
456
+ inputSchema: obj({}),
457
+ async run(node) {
458
+ const all = peers(node);
459
+ if (all.length === 0) {
460
+ return "No peers. Join a hub (VOLENET_MCP_HUB) or pair with a node directly (volenet_pair).";
461
+ }
462
+ return all.map((p) => {
463
+ const bits = [
464
+ p.connected ? "online " : "away ",
465
+ p.route === "direct" ? "direct" : `via ${p.viaHub}`,
466
+ p.name,
467
+ p.id.substring(0, 8)
468
+ ];
469
+ if (p.route === "hub" && !p.consented) bits.push("(no consent yet \u2014 volenet_connect)");
470
+ return ` ${bits.join(" ")}`;
471
+ }).join("\n");
472
+ }
473
+ },
474
+ {
475
+ name: "volenet_inbox",
476
+ description: "Messages that arrived for this session, including while it was not running, plus who tried to reach it while it was away. Reading marks them seen. Check this at the start of a session.",
477
+ inputSchema: obj({}),
478
+ async run(node) {
479
+ const unread = node.inbox.unread();
480
+ const lines = [];
481
+ if (unread.length === 0) lines.push("No new messages.");
482
+ else {
483
+ lines.push(`${unread.length} new message(s):`, "");
484
+ for (const m of unread) {
485
+ lines.push(` [${when2(m.ts)}] ${m.peerName} (${m.peerId.substring(0, 8)})`);
486
+ lines.push(` ${m.text.replace(/\n/g, "\n ")}`);
487
+ }
488
+ }
489
+ if (node.notices.length > 0) {
490
+ lines.push("", "Tried to reach you while you were away:");
491
+ for (const n of node.notices) {
492
+ lines.push(
493
+ ` ${n.fromName} (${n.from.substring(0, 8)}) \u2014 ${n.count}x, last ${when2(n.last)}`
494
+ );
495
+ }
496
+ lines.push(
497
+ " Their messages are held on their own device and arrive when they are next online."
498
+ );
499
+ }
500
+ await node.inbox.markRead();
501
+ return lines.join("\n");
502
+ }
503
+ },
504
+ {
505
+ name: "volenet_send",
506
+ description: "Send a message to a person or an agent. This is chat: it is delivered and read by whoever is there, and does NOT run their brain or wait for a reply. Use this to reach a person on the phone app. If the peer is offline the message waits here and goes out when they return.",
507
+ inputSchema: obj(
508
+ { to: str("Peer name or instance id (see volenet_peers)"), text: str("What to say") },
509
+ ["to", "text"]
510
+ ),
511
+ async run(node, args) {
512
+ const to = String(args.to ?? "");
513
+ const text = String(args.text ?? "");
514
+ if (!text.trim()) return "Nothing to send.";
515
+ const peer = resolve(node, to);
516
+ const res = await node.net.sendChat(peer?.id ?? to, text);
517
+ if (!res.ok) return `Not sent: ${res.error ?? "unknown error"}`;
518
+ await node.inbox.add({
519
+ peerId: peer?.id ?? to,
520
+ peerName: peer?.name ?? to,
521
+ dir: "out",
522
+ text,
523
+ ts: Date.now(),
524
+ id: `out-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
525
+ });
526
+ if (res.delivered === false) {
527
+ return `Held for ${peer?.name ?? to}: they are not reachable right now, and it goes out when they are back.`;
528
+ }
529
+ return `Sent to ${peer?.name ?? to}${res.relayed ? " (through a hub, sealed end to end)" : ""}.`;
530
+ }
531
+ },
532
+ {
533
+ name: "volenet_history",
534
+ description: "The conversation with one peer, oldest first.",
535
+ inputSchema: obj({
536
+ peer: str("Peer name or instance id"),
537
+ limit: num("Messages (default 50)")
538
+ }),
539
+ async run(node, args) {
540
+ const peer = resolve(node, String(args.peer ?? ""));
541
+ const id = peer?.id ?? String(args.peer ?? "");
542
+ const msgs = node.inbox.history(id, Number(args.limit ?? 50));
543
+ if (msgs.length === 0) return `Nothing recorded with ${peer?.name ?? id}.`;
544
+ return msgs.map((m) => `[${when2(m.ts)}] ${m.dir === "out" ? "you" : m.peerName}: ${m.text}`).join("\n");
545
+ }
546
+ },
547
+ {
548
+ name: "volenet_ask",
549
+ description: "Ask another AGENT's brain a question and wait for its answer. This runs the peer's model, so it takes as long as thinking takes, and the peer's operator must have granted this session brain access. Only works over a direct link \u2014 a hub will not carry it \u2014 and a person's phone app has no brain to ask; use volenet_send for people.",
550
+ inputSchema: obj(
551
+ {
552
+ to: str("Agent name or instance id"),
553
+ question: str("What to ask"),
554
+ timeout_ms: num("How long to wait (default 120000)")
555
+ },
556
+ ["to", "question"]
557
+ ),
558
+ async run(node, args) {
559
+ const peer = resolve(node, String(args.to ?? ""));
560
+ if (!peer) return `No peer found: "${args.to}". Use volenet_peers.`;
561
+ if (peer.route !== "direct") {
562
+ return `${peer.name} is only reachable through a hub, and a hub will not relay a question to an agent's brain \u2014 it carries chat and consent only. Use volenet_send, or pair directly with volenet_pair.`;
563
+ }
564
+ const mgr = node.net.getRemoteTaskManager();
565
+ if (!mgr) return "Remote task manager not available.";
566
+ const res = await mgr.delegateTask(
567
+ peer.id,
568
+ { taskId: "", input: String(args.question ?? ""), fromName: node.options.name },
569
+ Number(args.timeout_ms ?? 12e4)
570
+ );
571
+ if (res.status === "completed") return `${peer.name} says:
572
+
573
+ ${res.result}`;
574
+ const why = res.error ?? res.status;
575
+ if (typeof why === "string" && why.includes("allowBrain")) {
576
+ return `${peer.name} refused: ${why}
577
+
578
+ Its operator can allow this session by adding { "id": "${node.net.getKeyPair()?.instanceId}", "trust": "read", "allowBrain": true } to net.peers and restarting.`;
579
+ }
580
+ return `${peer.name} did not answer: ${why}`;
581
+ }
582
+ },
583
+ {
584
+ name: "volenet_requests",
585
+ description: "Trust decisions waiting on you: nodes asking to be trusted, and hub members asking to chat. Accept or deny one by naming it. Nothing is trusted until you say so.",
586
+ inputSchema: obj({
587
+ accept: str("Name or id to accept"),
588
+ deny: str("Name or id to deny")
589
+ }),
590
+ async run(node, args) {
591
+ const accept = args.accept ? String(args.accept) : void 0;
592
+ const deny = args.deny ? String(args.deny) : void 0;
593
+ const act = accept ?? deny;
594
+ if (act) {
595
+ const req = node.requests.find((r) => r.fromName === act || r.from.startsWith(act));
596
+ if (!req) return `No pending request matching "${act}".`;
597
+ const ok = accept ? req.kind === "pair" ? await node.net.acceptPair(req.from) : await node.net.approveRelayConnect(req.from) : req.kind === "pair" ? await node.net.denyPair(req.from) : await node.net.denyRelayConnect(req.from);
598
+ node.requests.splice(node.requests.indexOf(req), 1);
599
+ return ok.ok ? `${accept ? "Accepted" : "Denied"} ${req.fromName}.` : `Failed: ${"error" in ok ? ok.error : "unknown"}`;
600
+ }
601
+ const pairs = node.net.listPairRequests();
602
+ for (const p of pairs) {
603
+ if (!node.requests.some((r) => r.from === p.id)) {
604
+ node.requests.push({ kind: "pair", from: p.id, fromName: p.name, at: Date.now() });
605
+ }
606
+ }
607
+ if (node.requests.length === 0) return "Nothing waiting.";
608
+ return node.requests.map(
609
+ (r) => ` ${r.kind === "pair" ? "wants to be trusted" : "wants to chat "} ${r.fromName} ${r.from.substring(0, 8)}${r.note ? ` \u2014 "${r.note}"` : ""}`
610
+ ).join("\n");
611
+ }
612
+ },
613
+ {
614
+ name: "volenet_wait",
615
+ description: "Wait for the next message to arrive, instead of checking again later. Use it whenever you have said something and a reply is expected \u2014 it turns a mailbox into a conversation. Returns as soon as anything lands, or reports that nothing came within the time given. Nothing is lost either way: a message that arrives after you stop waiting is still in the inbox.",
616
+ inputSchema: obj({
617
+ from: str("Only wake for this peer, by name or id. Omit to wake for anyone."),
618
+ timeout_ms: num("How long to wait. Default 60000, maximum 300000.")
619
+ }),
620
+ async run(node, args) {
621
+ const limit = Math.min(Math.max(Number(args.timeout_ms ?? 6e4), 1e3), 3e5);
622
+ const want = args.from ? resolve(node, String(args.from)) : void 0;
623
+ const wanted = (m) => !args.from || m.peerId === (want?.id ?? args.from);
624
+ const already = node.inbox.unread().filter(wanted);
625
+ if (already.length === 0) {
626
+ await new Promise((done) => {
627
+ const timer = setTimeout(() => {
628
+ off();
629
+ done();
630
+ }, limit);
631
+ const off = node.onMessage((m) => {
632
+ if (!wanted(m)) return;
633
+ clearTimeout(timer);
634
+ off();
635
+ done();
636
+ });
637
+ });
638
+ }
639
+ const arrived = node.inbox.unread().filter(wanted);
640
+ await node.inbox.markRead();
641
+ if (arrived.length === 0) {
642
+ return `Nothing arrived within ${Math.round(limit / 1e3)}s${args.from ? ` from ${want?.name ?? args.from}` : ""}. It is not lost \u2014 whatever they send lands in the inbox whenever it comes.`;
643
+ }
644
+ return arrived.map((m) => `[${when2(m.ts)}] ${m.peerName}: ${m.text}`).join("\n");
645
+ }
646
+ },
647
+ {
648
+ name: "volenet_hub",
649
+ description: "Join a hub, leave one, or report which hub this session is on. A hub makes you reachable by people and agents that cannot dial your machine \u2014 it carries sealed traffic it cannot read, and stores no message. Called with no arguments it just reports. The choice is remembered, so the next session starts where this one left off.",
650
+ inputSchema: obj({
651
+ url: str("Hub URL to join, e.g. https://hub.example.com/mesh"),
652
+ leave: { type: "boolean", description: "Leave the current hub" }
653
+ }),
654
+ async run(node, args) {
655
+ if (args.leave) {
656
+ const hub = node.options.hub;
657
+ if (!hub) return "Not on a hub.";
658
+ node.net.forgetPeer(hub);
659
+ node.options.hub = void 0;
660
+ node.hubStatus = "no hub configured";
661
+ await saveStored(node.options.dir, { hub: void 0 });
662
+ return `Left ${hub}. Your identity and everyone you have paired with directly are untouched.`;
663
+ }
664
+ if (!args.url) {
665
+ return node.options.hub ? `${node.hubStatus}
666
+
667
+ Call with leave:true to come off it, or url to move to another.` : "Not on a hub. Give a url to join one \u2014 or stay off it and pair directly with volenet_connect.";
668
+ }
669
+ const url = String(args.url).replace(/\/$/, "");
670
+ if (await alreadyTrusts(node.options.dir, url)) {
671
+ await node.net.addPeer(url);
672
+ node.options.hub = url;
673
+ node.hubStatus = `joined ${url}`;
674
+ await saveStored(node.options.dir, { hub: url });
675
+ return `Joined ${url} again \u2014 it was already trusted, so no introduction was needed.`;
676
+ }
677
+ const res = await node.net.initiateJoin(url);
678
+ if (!res.ok) return `Could not join ${url}: ${res.error}`;
679
+ node.options.hub = url;
680
+ node.hubStatus = res.pending ? `waiting for approval at ${url}` : `joined ${res.hubName ?? url}`;
681
+ await saveStored(node.options.dir, { hub: url });
682
+ return res.pending ? `Asked to join ${url}. Its operator has to approve before you appear in the roster.` : `Joined ${res.hubName ?? url}. Remembered, so the next session starts here. Call volenet_peers to see who is around.`;
683
+ }
684
+ },
685
+ {
686
+ name: "volenet_connect",
687
+ description: "Reach out to someone new: pair directly with a node at a URL, or ask a hub member for consent to chat. Pairing is two calls \u2014 the first reports the fingerprint of whoever answers, the second confirms it \u2014 because trusting a URL blind is trusting whoever holds it. Neither side trusts you until they accept.",
688
+ inputSchema: obj({
689
+ url: str("Node URL to pair with directly, e.g. http://10.0.0.5:9700"),
690
+ confirm: str("The fingerprint returned by a first call with url, confirming who answers"),
691
+ member: str("Hub member name or id to ask for chat consent"),
692
+ note: str("A line saying who you are")
693
+ }),
694
+ async run(node, args) {
695
+ const note = args.note ? String(args.note) : void 0;
696
+ if (args.url) {
697
+ const url = String(args.url);
698
+ const probe = await node.net.probePair(url);
699
+ if (!probe.ok || !probe.publicKey) return `Could not reach it: ${probe.error}`;
700
+ const confirm = args.confirm ? String(args.confirm).trim() : "";
701
+ if (!confirm) {
702
+ return [
703
+ `${probe.name ?? url} answers with fingerprint:`,
704
+ ` ${probe.fingerprint}`,
705
+ probe.alreadyTrusted ? " (already trusted by this session)" : "",
706
+ "",
707
+ "Check that against what the other side reports, then call this again with",
708
+ "confirm set to that fingerprint to trust it and send the pair request."
709
+ ].filter(Boolean).join("\n");
710
+ }
711
+ if (!probe.fingerprint?.startsWith(confirm)) {
712
+ return `That fingerprint does not match: it answers with ${probe.fingerprint}. Nothing was trusted.`;
713
+ }
714
+ const res = await node.net.initiatePair(url, probe.publicKey, note);
715
+ return res.ok ? `Trusted ${probe.name ?? url} and asked it to trust this session. Nothing arrives until their operator accepts.` : `Could not ask: ${res.error}`;
716
+ }
717
+ if (args.member) {
718
+ const res = await node.net.requestRelayConnect(String(args.member), note);
719
+ if (!res.ok) return `Could not ask: ${res.error}`;
720
+ return res.queued ? `${args.member} is away \u2014 the request waits here and goes out when they are back.` : `Asked ${args.member} for consent to chat.`;
721
+ }
722
+ return "Give either a url (direct pairing) or a member (hub consent).";
723
+ }
724
+ }
725
+ ];
726
+
727
+ // src/index.ts
728
+ function unreadFooter(node, toolName) {
729
+ if (toolName === "volenet_inbox" || toolName === "volenet_wait") return "";
730
+ const unread = node.inbox.unread();
731
+ if (unread.length === 0) return "";
732
+ const who = [...new Set(unread.map((m) => m.peerName))].join(", ");
733
+ return `
734
+
735
+ \u2014 ${unread.length} unread message${unread.length === 1 ? "" : "s"} from ${who}. Read them with volenet_inbox.`;
736
+ }
737
+ function createServer(node) {
738
+ const server = new Server({ name: "volenet", version: "0.1.0" }, { capabilities: { tools: {} } });
739
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
740
+ tools: TOOLS.map((t) => ({
741
+ name: t.name,
742
+ description: t.description,
743
+ inputSchema: t.inputSchema
744
+ }))
745
+ }));
746
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
747
+ const tool = TOOLS.find((t) => t.name === request.params.name);
748
+ if (!tool) {
749
+ return {
750
+ content: [{ type: "text", text: `No such tool: ${request.params.name}` }],
751
+ isError: true
752
+ };
753
+ }
754
+ try {
755
+ const text = await tool.run(node, request.params.arguments ?? {});
756
+ return { content: [{ type: "text", text: text + unreadFooter(node, tool.name) }] };
757
+ } catch (err) {
758
+ return {
759
+ content: [
760
+ { type: "text", text: err instanceof Error ? err.message : String(err) }
761
+ ],
762
+ isError: true
763
+ };
764
+ }
765
+ });
766
+ return server;
767
+ }
768
+ async function main() {
769
+ const options = await resolveSettings();
770
+ const node = await startNode(options);
771
+ const server = createServer(node);
772
+ await server.connect(new StdioServerTransport());
773
+ process.stderr.write(
774
+ `volenet-mcp: ${options.name} (${node.net.getKeyPair()?.instanceId.substring(0, 8)}) ready${options.hub ? ` \u2014 hub ${options.hub}` : " \u2014 no hub configured"}
775
+ `
776
+ );
777
+ let stopping = false;
778
+ const shutdown = async () => {
779
+ if (stopping) return;
780
+ stopping = true;
781
+ await node.stop().catch(() => void 0);
782
+ process.exit(0);
783
+ };
784
+ process.on("SIGINT", shutdown);
785
+ process.on("SIGTERM", shutdown);
786
+ process.stdin.on("close", shutdown);
787
+ }
788
+ if (process.argv[1]?.includes("volenet-mcp") || process.env.VOLENET_MCP_RUN === "1") {
789
+ if (process.argv[2] || process.stdin.isTTY) {
790
+ run(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
791
+ process.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}
792
+ `);
793
+ process.exit(1);
794
+ });
795
+ } else {
796
+ main().catch((err) => {
797
+ process.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}
798
+ `);
799
+ process.exit(1);
800
+ });
801
+ }
802
+ }
803
+ export {
804
+ Inbox,
805
+ TOOLS,
806
+ createServer,
807
+ defaultDir,
808
+ defaultName,
809
+ loadStored,
810
+ resolveSettings,
811
+ run as runCli,
812
+ saveStored,
813
+ startNode,
814
+ unreadFooter
815
+ };
816
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/config.ts","../src/inbox.ts","../src/install.ts","../src/node.ts","../src/tools.ts"],"sourcesContent":["/**\n * VoleNet as an MCP server.\n *\n * Claude Code is very good inside one machine and one session. It has no way to reach a person on\n * their phone, no way to talk to an agent someone else owns, and no identity that outlives the\n * session. VoleNet has all three and none of it is coding-assistant work — signed identity, hybrid\n * post-quantum sealing, a hub that carries ciphertext it cannot read, consent, and hold-and-forward\n * for a peer that is not there right now.\n *\n * So this is not another agent. It is the network, handed to an agent that already exists.\n *\n * stdio transport, which means **stdout belongs to the protocol**: anything printed there that is\n * not a JSON-RPC frame breaks the client. The core logger is silent by default and writes to a file\n * when VOLE_LOG_FILE is set; diagnostics here go to stderr.\n */\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { run as runCli } from './cli.js'\nimport { type Node, resolveSettings, startNode } from './node.js'\nimport { TOOLS } from './tools.js'\n\nexport { Inbox } from './inbox.js'\nexport { run as runCli } from './cli.js'\nexport { type Settings, defaultDir, defaultName, loadStored, saveStored } from './config.js'\nexport { type Node, type NodeOptions, resolveSettings, startNode } from './node.js'\nexport { TOOLS, type ToolDef } from './tools.js'\n\n/**\n * What is waiting, appended to every tool's result.\n *\n * MCP has no way for a server to push, so an arrived message would otherwise sit unseen until\n * somebody thought to look. Saying so on every result means any use of any tool surfaces it —\n * ambient awareness in place of the notification the protocol cannot send. The tools that just\n * showed you the messages are excluded, since they leave nothing unread.\n */\nexport function unreadFooter(node: Node, toolName: string): string {\n\tif (toolName === 'volenet_inbox' || toolName === 'volenet_wait') return ''\n\tconst unread = node.inbox.unread()\n\tif (unread.length === 0) return ''\n\tconst who = [...new Set(unread.map((m) => m.peerName))].join(', ')\n\treturn `\\n\\n— ${unread.length} unread message${unread.length === 1 ? '' : 's'} from ${who}. Read them with volenet_inbox.`\n}\n\n/** Wire the tools to an MCP server. Separated so a test can drive it without a transport. */\nexport function createServer(node: Node): Server {\n\tconst server = new Server({ name: 'volenet', version: '0.1.0' }, { capabilities: { tools: {} } })\n\n\tserver.setRequestHandler(ListToolsRequestSchema, async () => ({\n\t\ttools: TOOLS.map((t) => ({\n\t\t\tname: t.name,\n\t\t\tdescription: t.description,\n\t\t\tinputSchema: t.inputSchema as { type: 'object' },\n\t\t})),\n\t}))\n\n\tserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n\t\tconst tool = TOOLS.find((t) => t.name === request.params.name)\n\t\tif (!tool) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text' as const, text: `No such tool: ${request.params.name}` }],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tconst text = await tool.run(node, (request.params.arguments ?? {}) as Record<string, unknown>)\n\t\t\treturn { content: [{ type: 'text' as const, text: text + unreadFooter(node, tool.name) }] }\n\t\t} catch (err) {\n\t\t\t// A failed tool is a result, not a crash: the session should see why and carry on.\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{ type: 'text' as const, text: err instanceof Error ? err.message : String(err) },\n\t\t\t\t],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\t})\n\n\treturn server\n}\n\nasync function main(): Promise<void> {\n\tconst options = await resolveSettings()\n\tconst node = await startNode(options)\n\tconst server = createServer(node)\n\tawait server.connect(new StdioServerTransport())\n\tprocess.stderr.write(\n\t\t`volenet-mcp: ${options.name} (${node.net.getKeyPair()?.instanceId.substring(0, 8)}) ready` +\n\t\t\t`${options.hub ? ` — hub ${options.hub}` : ' — no hub configured'}\\n`,\n\t)\n\n\tlet stopping = false\n\tconst shutdown = async () => {\n\t\tif (stopping) return\n\t\tstopping = true\n\t\tawait node.stop().catch(() => undefined)\n\t\tprocess.exit(0)\n\t}\n\tprocess.on('SIGINT', shutdown)\n\tprocess.on('SIGTERM', shutdown)\n\t// The client closing its end is the ordinary way this ends.\n\tprocess.stdin.on('close', shutdown)\n}\n\n// Only when run as the binary, so importing this module in a test starts nothing.\nif (process.argv[1]?.includes('volenet-mcp') || process.env.VOLENET_MCP_RUN === '1') {\n\t// A bare invocation is the MCP server over stdio, which is how a client starts it — but a\n\t// person who runs it in a terminal means the opposite, and would otherwise get a process that\n\t// looks hung, or a port conflict from a server they did not know they had started. A client\n\t// attaches a pipe, never a TTY, so that is the honest way to tell them apart.\n\tif (process.argv[2] || process.stdin.isTTY) {\n\t\trunCli(process.argv.slice(2))\n\t\t\t.then((code) => process.exit(code))\n\t\t\t.catch((err) => {\n\t\t\t\tprocess.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}\\n`)\n\t\t\t\tprocess.exit(1)\n\t\t\t})\n\t} else {\n\t\tmain().catch((err) => {\n\t\t\tprocess.stderr.write(`volenet-mcp: ${err instanceof Error ? err.message : String(err)}\\n`)\n\t\t\tprocess.exit(1)\n\t\t})\n\t}\n}\n","import * as path from 'node:path'\n/**\n * The command line, for the things you want before a session exists — or without one.\n *\n * Everything here works on files alone: no node is started, no port is bound, nothing dials out.\n * That is deliberate. The MCP server may well be running while you type these, and a second node\n * on the same identity would fight it for the port; and a `SessionStart` hook has to be safe to\n * run every single time a session opens.\n *\n * So `hub` records a choice that the next node start acts on, and `inbox` reads what has already\n * been written. Anything that genuinely needs the network — the roster, pairing, asking a brain —\n * is a tool, because it needs a live node and a conversation to happen in.\n */\nimport { loadKeyPair } from '@openvole/volenet'\nimport { defaultDir, defaultName, loadStored, saveStored } from './config.js'\nimport { Inbox } from './inbox.js'\nimport { install } from './install.js'\n\nconst USAGE = `volenet-mcp — VoleNet as an MCP server\n\n volenet-mcp install [--user] register with Claude Code (default: this project)\n volenet-mcp whoami this machine's identity on the mesh\n volenet-mcp hub [url|--leave] which hub to use; takes effect on the next session\n volenet-mcp inbox [--read] messages waiting, for catching up or a SessionStart hook\n\nWith no command it runs as the MCP server itself, over stdio, which is how Claude Code starts it.\nAnything needing a live node — peers, pairing, asking an agent's brain — is a tool you ask for in\na session, not a command here.\n`\n\nconst when = (ts: number) => new Date(ts).toISOString().replace('T', ' ').slice(0, 16)\n\nexport async function run(argv: string[], out = process.stdout): Promise<number> {\n\tconst [command, ...rest] = argv\n\tconst dir = defaultDir()\n\n\tif (!command || command === 'help' || command === '--help' || command === '-h') {\n\t\tout.write(USAGE)\n\t\treturn 0\n\t}\n\n\tif (command === 'install') return install(rest, out)\n\n\tif (command === 'whoami') {\n\t\tconst stored = await loadStored(dir)\n\t\tconst keys = await loadKeyPair(path.join(dir, 'net')).catch(() => null)\n\t\tif (!keys) {\n\t\t\tout.write(\n\t\t\t\t`No identity yet at ${dir}.\\nOne is generated the first time the server runs — start a session, or ask for volenet_whoami.\\n`,\n\t\t\t)\n\t\t\treturn 0\n\t\t}\n\t\tout.write(\n\t\t\t[\n\t\t\t\t`name ${stored.name ?? defaultName()}`,\n\t\t\t\t`instanceId ${keys.instanceId}`,\n\t\t\t\t`hub ${stored.hub ?? '(none — set one with: volenet-mcp hub <url>)'}`,\n\t\t\t\t`store ${dir}`,\n\t\t\t\t'',\n\t\t\t\t'That directory is your identity. Back it up; anyone who has it is you.',\n\t\t\t\t'',\n\t\t\t].join('\\n'),\n\t\t)\n\t\treturn 0\n\t}\n\n\tif (command === 'hub') {\n\t\tconst stored = await loadStored(dir)\n\t\tif (rest.includes('--leave')) {\n\t\t\tif (!stored.hub) {\n\t\t\t\tout.write('Not on a hub.\\n')\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tawait saveStored(dir, { hub: undefined })\n\t\t\tout.write(`Left ${stored.hub}. Your identity and direct pairings are untouched.\\n`)\n\t\t\treturn 0\n\t\t}\n\t\tconst url = rest.find((a) => !a.startsWith('-'))\n\t\tif (!url) {\n\t\t\tout.write(stored.hub ? `${stored.hub}\\n` : 'No hub set. Give one: volenet-mcp hub <url>\\n')\n\t\t\treturn 0\n\t\t}\n\t\tawait saveStored(dir, { hub: url.replace(/\\/$/, '') })\n\t\tout.write(\n\t\t\t`Hub set to ${url}.\\nIt is joined the next time the server starts — restart Claude Code, or ask for volenet_hub to do it now.\\n`,\n\t\t)\n\t\treturn 0\n\t}\n\n\tif (command === 'inbox') {\n\t\tconst inbox = new Inbox(path.join(dir, 'inbox.json'))\n\t\tawait inbox.load()\n\t\tconst unread = inbox.unread()\n\t\tif (unread.length === 0) {\n\t\t\tout.write('No new messages.\\n')\n\t\t\treturn 0\n\t\t}\n\t\tout.write(`${unread.length} new VoleNet message${unread.length === 1 ? '' : 's'}:\\n\\n`)\n\t\tfor (const m of unread) {\n\t\t\tout.write(` [${when(m.ts)}] ${m.peerName}: ${m.text}\\n`)\n\t\t}\n\t\t// Marking is opt-in: a hook that puts these in context should mark them, an operator\n\t\t// glancing at the inbox should not silently hide them from the next session.\n\t\tif (rest.includes('--read')) await inbox.markRead()\n\t\tout.write('\\n')\n\t\treturn 0\n\t}\n\n\tout.write(`Unknown command: ${command}\\n\\n${USAGE}`)\n\treturn 1\n}\n","/**\n * Settings that survive a restart, so nothing has to be configured at install time.\n *\n * The first version of this took its name, hub and port from environment variables, which meant\n * the install line carried three flags a new user could not yet know the values of — and changing\n * one meant re-registering the server. Settings belong to the node, not to the command that\n * launches it: they live in its data directory, next to the identity they describe, and are\n * changed from inside a session with `volenet_hub`.\n *\n * Environment still wins where it is set, for scripted setups and CI. Nothing is required.\n */\nimport * as fs from 'node:fs/promises'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\n\nexport interface StoredConfig {\n\tname?: string\n\thub?: string\n\tport?: number\n}\n\nexport interface Settings {\n\tname: string\n\thub?: string\n\tdir: string\n\tport: number\n}\n\nexport function defaultDir(): string {\n\treturn process.env.VOLENET_MCP_DIR?.trim() || path.join(os.homedir(), '.openvole', 'volenet-mcp')\n}\n\n/** A name that says what this is without needing to be chosen. Identity is the key, not this. */\nexport function defaultName(): string {\n\treturn `claude-${os.hostname().split('.')[0].toLowerCase()}`\n}\n\nconst file = (dir: string) => path.join(dir, 'config.json')\n\nexport async function loadStored(dir: string): Promise<StoredConfig> {\n\ttry {\n\t\tconst raw = JSON.parse(await fs.readFile(file(dir), 'utf-8')) as StoredConfig\n\t\treturn raw && typeof raw === 'object' ? raw : {}\n\t} catch {\n\t\treturn {}\n\t}\n}\n\nexport async function saveStored(dir: string, patch: StoredConfig): Promise<StoredConfig> {\n\tconst next = { ...(await loadStored(dir)), ...patch }\n\tfor (const k of Object.keys(next) as Array<keyof StoredConfig>) {\n\t\tif (next[k] === undefined) delete next[k]\n\t}\n\tawait fs.mkdir(dir, { recursive: true })\n\tawait fs.writeFile(file(dir), `${JSON.stringify(next, null, 2)}\\n`, 'utf-8')\n\treturn next\n}\n\n/** Environment over stored settings over defaults. Every layer is optional. */\nexport async function resolveSettings(): Promise<Settings> {\n\tconst dir = defaultDir()\n\tconst stored = await loadStored(dir)\n\tconst envPort = Number(process.env.VOLENET_MCP_PORT)\n\treturn {\n\t\tname: process.env.VOLENET_MCP_NAME?.trim() || stored.name || defaultName(),\n\t\thub: process.env.VOLENET_MCP_HUB?.trim() || stored.hub || undefined,\n\t\tdir,\n\t\tport: (Number.isFinite(envPort) && envPort > 0 ? envPort : stored.port) || 9750,\n\t}\n}\n","/**\n * What was said, kept across sessions.\n *\n * An MCP server lives and dies with the editor session that spawned it, but a conversation does\n * not. VoleNet already makes an intermittent peer work — a sender holds what it could not deliver\n * and flushes when you reappear, and a hub hands you notices about who tried — so the only thing\n * missing is somewhere to put what arrives, that is still there next time.\n *\n * A flat log rather than a per-peer file: the useful question is almost always \"what came in while\n * I was gone\", across everyone, and that is one scan rather than a directory walk.\n */\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\n\nexport interface Message {\n\t/** The peer this is with — the sender for 'in', the recipient for 'out'. */\n\tpeerId: string\n\tpeerName: string\n\tdir: 'in' | 'out'\n\ttext: string\n\t/** Milliseconds since the epoch, from the signed message for 'in'. */\n\tts: number\n\t/** The signed message id, so a replay or a double-flush cannot duplicate a line. */\n\tid: string\n}\n\n/** Kept small enough to hold in memory and cheap to rewrite; the oldest go first. */\nexport const MAX_MESSAGES = 2000\n\nexport class Inbox {\n\tprivate messages: Message[] = []\n\t/** Per peer, the timestamp up to which the session has been shown its messages. */\n\tprivate readAt = new Map<string, number>()\n\tprivate writing: Promise<void> = Promise.resolve()\n\n\tconstructor(private readonly file: string) {}\n\n\tasync load(): Promise<void> {\n\t\ttry {\n\t\t\tconst raw = JSON.parse(await fs.readFile(this.file, 'utf-8')) as {\n\t\t\t\tmessages?: Message[]\n\t\t\t\treadAt?: Record<string, number>\n\t\t\t}\n\t\t\tthis.messages = (raw.messages ?? []).filter(\n\t\t\t\t(m) => m && typeof m.peerId === 'string' && typeof m.text === 'string',\n\t\t\t)\n\t\t\tthis.readAt = new Map(Object.entries(raw.readAt ?? {}))\n\t\t} catch {\n\t\t\tthis.messages = []\n\t\t\tthis.readAt = new Map()\n\t\t}\n\t}\n\n\t/** Record a message. Returns false when this id was already recorded. */\n\tasync add(m: Message): Promise<boolean> {\n\t\tif (this.messages.some((x) => x.id === m.id)) return false\n\t\tthis.messages.push(m)\n\t\tif (this.messages.length > MAX_MESSAGES) {\n\t\t\tthis.messages.splice(0, this.messages.length - MAX_MESSAGES)\n\t\t}\n\t\tawait this.persist()\n\t\treturn true\n\t}\n\n\t/** Everything with one peer, oldest first. */\n\thistory(peerId: string, limit = 50): Message[] {\n\t\treturn this.messages.filter((m) => m.peerId === peerId).slice(-limit)\n\t}\n\n\t/** Inbound messages the session has not been shown yet, oldest first. */\n\tunread(): Message[] {\n\t\treturn this.messages.filter((m) => m.dir === 'in' && m.ts > (this.readAt.get(m.peerId) ?? 0))\n\t}\n\n\t/** Mark everything currently unread as seen. */\n\tasync markRead(): Promise<void> {\n\t\tfor (const m of this.unread()) {\n\t\t\tconst at = this.readAt.get(m.peerId) ?? 0\n\t\t\tif (m.ts > at) this.readAt.set(m.peerId, m.ts)\n\t\t}\n\t\tawait this.persist()\n\t}\n\n\t/** Every peer we have said anything to or heard anything from, most recent first. */\n\tpeers(): Array<{ peerId: string; peerName: string; last: number; unread: number }> {\n\t\tconst by = new Map<string, { peerId: string; peerName: string; last: number; unread: number }>()\n\t\tfor (const m of this.messages) {\n\t\t\tconst e = by.get(m.peerId) ?? { peerId: m.peerId, peerName: m.peerName, last: 0, unread: 0 }\n\t\t\tif (m.peerName) e.peerName = m.peerName\n\t\t\te.last = Math.max(e.last, m.ts)\n\t\t\tif (m.dir === 'in' && m.ts > (this.readAt.get(m.peerId) ?? 0)) e.unread++\n\t\t\tby.set(m.peerId, e)\n\t\t}\n\t\treturn [...by.values()].sort((a, b) => b.last - a.last)\n\t}\n\n\tget size(): number {\n\t\treturn this.messages.length\n\t}\n\n\t/** Serialised: two messages arriving together must not race each other's rewrite. */\n\tprivate persist(): Promise<void> {\n\t\tthis.writing = this.writing.then(async () => {\n\t\t\tconst body = JSON.stringify(\n\t\t\t\t{ messages: this.messages, readAt: Object.fromEntries(this.readAt) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)\n\t\t\tawait fs.mkdir(path.dirname(this.file), { recursive: true })\n\t\t\tconst tmp = `${this.file}.tmp`\n\t\t\tawait fs.writeFile(tmp, body, 'utf-8')\n\t\t\tawait fs.rename(tmp, this.file)\n\t\t})\n\t\treturn this.writing\n\t}\n}\n","/**\n * `npx @openvole/volenet-mcp install` — register this server with Claude Code.\n *\n * Setup used to be a line carrying three environment flags whose values a new user had no way to\n * know yet: their name on the mesh, a hub, a port. None of that belongs in an install command. The\n * name has a sensible default, the port only matters to peers that can dial you, and the hub is a\n * decision better made from inside a session, where `volenet_hub` joins one and remembers it.\n *\n * So this takes no configuration. It shells out to the `claude` CLI when that is on PATH, and\n * otherwise prints the line to paste — guessing at another tool's config file is how you corrupt\n * one. It never inherits stdin: an installer that can block on a prompt is not a one-shot command.\n */\nimport { spawnSync } from 'node:child_process'\nimport * as path from 'node:path'\n\nexport const SERVER_NAME = 'volenet'\nconst PACKAGE = '@openvole/volenet-mcp'\n\n/**\n * How this server should be launched, from wherever `install` was itself run.\n *\n * Not simply \"is this a .js file\": run through `npx`, the entry *is* a .js file, but one living\n * in a transient cache that npm is free to evict — registering that path would work until it\n * suddenly did not. What distinguishes the cases is `node_modules`: an installed copy is always\n * under one and should be launched by package name, while a build in a working tree is not and\n * has to be launched by path, since there is nothing published to resolve.\n */\nexport function launchCommand(entry = process.argv[1]): string[] {\n\tconst installed =\n\t\t!entry || !entry.endsWith('.js') || entry.includes(`${path.sep}node_modules${path.sep}`)\n\treturn installed ? ['npx', '-y', PACKAGE] : ['node', entry]\n}\n\n/** The `claude mcp add` arguments, as a pure value so a test can check them without running one. */\nexport function addArgs(scope: 'user' | 'local', command: string[]): string[] {\n\treturn ['mcp', 'add', SERVER_NAME, '-s', scope, '--', ...command]\n}\n\nconst NEXT_STEPS =\n\t'\\nRestart Claude Code — MCP servers load at startup — then:\\n\\n' +\n\t' volenet_whoami who you are on the mesh (an identity is made on first run)\\n' +\n\t' volenet_hub url:\"...\" join a hub, to be reachable from anywhere\\n' +\n\t' volenet_connect url:\"...\" or pair directly with an agent you can dial\\n\\n' +\n\t'Nothing else needs configuring.\\n'\n\nexport function install(argv: string[], out = process.stdout): number {\n\tconst scope = argv.includes('--user') ? 'user' : 'local'\n\tconst command = launchCommand()\n\tconst paste = `claude mcp add ${SERVER_NAME} -s ${scope} -- ${command.join(' ')}`\n\n\t// Never inherit stdin: if the CLI asks something, this would hang instead of installing.\n\tconst run = (args: string[]) =>\n\t\tspawnSync('claude', args, {\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t\tencoding: 'utf-8',\n\t\t\ttimeout: 30_000,\n\t\t})\n\n\tconst listed = run(['mcp', 'list'])\n\tif (listed.error) {\n\t\tout.write(\n\t\t\t`The \\`claude\\` CLI is not on PATH. Run this once, in the project you want it in:\\n\\n ${paste}\\n\\n`,\n\t\t)\n\t\treturn 1\n\t}\n\tif (listed.stdout?.includes(`${SERVER_NAME}:`)) {\n\t\tout.write(`${SERVER_NAME} is already registered — nothing to do.\\n${NEXT_STEPS}`)\n\t\treturn 0\n\t}\n\n\tconst added = run(addArgs(scope, command))\n\tif (added.status !== 0) {\n\t\tout.write(\n\t\t\t`Could not register it automatically${added.stderr ? `: ${added.stderr.trim()}` : ''}\\n\\n` +\n\t\t\t\t`Run this once instead:\\n\\n ${paste}\\n\\n`,\n\t\t)\n\t\treturn added.status ?? 1\n\t}\n\tout.write(`Registered ${SERVER_NAME} (${scope} scope).\\n${NEXT_STEPS}`)\n\treturn 0\n}\n","/**\n * The VoleNet node this server *is*.\n *\n * A Claude Code session gets its own identity on the mesh rather than borrowing an agent's: its\n * own keypair, its own name, its own consent decisions. That is the whole point — \"my agent\" and\n * \"your agent\" have to be different principals before anything between them means much.\n *\n * The node lives only while the editor session does, which VoleNet already handles: a sender holds\n * what it could not deliver and flushes when we reappear, and a hub hands us notices about who\n * tried while we were gone. So an intermittent peer is a supported peer, not a degraded one.\n */\nimport * as path from 'node:path'\nimport {\n\tVoleNetManager,\n\tcreateEventBus,\n\tloadAuthorizedVoles,\n\tparsePublicKey,\n} from '@openvole/volenet'\nimport { type Settings, resolveSettings } from './config.js'\nimport { Inbox, type Message } from './inbox.js'\n\n/** What a node needs to start. Resolved from stored settings, env and defaults. */\nexport type NodeOptions = Settings\n\nexport { resolveSettings }\n\nexport interface PendingRequest {\n\tkind: 'pair' | 'relay'\n\tfrom: string\n\tfromName: string\n\tnote?: string\n\tat: number\n}\n\nexport interface Notice {\n\tfrom: string\n\tfromName: string\n\tcount: number\n\tlast: number\n}\n\nexport interface Node {\n\tnet: VoleNetManager\n\tinbox: Inbox\n\t/** What happened when we tried to join the configured hub, for whoami to report honestly. */\n\thubStatus: string\n\t/** Trust decisions waiting on the person, newest last. */\n\trequests: PendingRequest[]\n\t/** Who tried to reach us while we were away, as the hub reports on reconnect. */\n\tnotices: Notice[]\n\toptions: NodeOptions\n\t/**\n\t * Be told when a message lands. MCP cannot push, so a session that wants to *wait* for a reply\n\t * — rather than poll for one — needs somewhere to hang a promise. Returns an unsubscribe.\n\t */\n\tonMessage: (fn: (m: Message) => void) => () => void\n\tstop: () => Promise<void>\n}\n\nexport async function startNode(options: NodeOptions): Promise<Node> {\n\tconst bus = createEventBus()\n\tconst inbox = new Inbox(path.join(options.dir, 'inbox.json'))\n\tawait inbox.load()\n\n\tconst requests: PendingRequest[] = []\n\tconst notices: Notice[] = []\n\n\tconst net = new VoleNetManager(\n\t\t{\n\t\t\tenabled: true,\n\t\t\tinstanceName: options.name,\n\t\t\trole: 'peer',\n\t\t\tport: options.port,\n\t\t\tkeyPath: path.join(options.dir, 'net', 'vole_key'),\n\t\t\t// A hub is a peer we dial. 'read' rather than 'full': a hub carries our sealed traffic,\n\t\t\t// it has no business acting on this node.\n\t\t\tpeers: options.hub ? [{ url: options.hub, trust: 'read' }] : [],\n\t\t},\n\t\toptions.dir,\n\t)\n\n\tconst listeners = new Set<(m: Message) => void>()\n\tbus.on('volenet:chat', (d) => {\n\t\tconst m = d as {\n\t\t\tfrom: string\n\t\t\tfromName: string\n\t\t\ttext: string\n\t\t\tmessageId: string\n\t\t\ttimestamp: number\n\t\t}\n\t\tconst message: Message = {\n\t\t\tpeerId: m.from,\n\t\t\tpeerName: m.fromName,\n\t\t\tdir: 'in',\n\t\t\ttext: m.text,\n\t\t\tts: m.timestamp,\n\t\t\tid: m.messageId,\n\t\t}\n\t\t// Only a message we had not already recorded wakes a waiter, so a replay cannot.\n\t\tvoid inbox.add(message).then((added) => {\n\t\t\tif (added) for (const fn of listeners) fn(message)\n\t\t})\n\t})\n\n\tbus.on('volenet:chat:pending', (d) => {\n\t\tconst p = d as { from: Array<{ from: string; fromName: string; count: number; last: number }> }\n\t\tfor (const n of p.from ?? []) {\n\t\t\tconst at = notices.findIndex((x) => x.from === n.from)\n\t\t\tif (at >= 0) notices[at] = n\n\t\t\telse notices.push(n)\n\t\t}\n\t})\n\n\tconst remember = (kind: 'pair' | 'relay') => (d: unknown) => {\n\t\tconst r = d as { from: string; fromName: string; note?: string }\n\t\tif (requests.some((x) => x.from === r.from && x.kind === kind)) return\n\t\trequests.push({ kind, from: r.from, fromName: r.fromName, note: r.note, at: Date.now() })\n\t}\n\tbus.on('volenet:pair:request', remember('pair'))\n\tbus.on('volenet:relay:request', remember('relay'))\n\n\tawait net.start(undefined, bus)\n\n\t// Dialling a hub we have never met gets a 401: it has no reason to trust this key yet. The\n\t// join flow is what introduces us, and it hands back the hub's own key to pin — the half that\n\t// matters, since from then on only that key may sign hub traffic to us.\n\tlet hubStatus = 'no hub configured'\n\tif (options.hub) {\n\t\thubStatus = (await alreadyTrusts(options.dir, options.hub))\n\t\t\t? `joined ${options.hub}`\n\t\t\t: await join(net, options.hub)\n\t}\n\n\treturn {\n\t\tnet,\n\t\tinbox,\n\t\trequests,\n\t\tnotices,\n\t\toptions,\n\t\thubStatus,\n\t\tonMessage: (fn) => {\n\t\t\tlisteners.add(fn)\n\t\t\treturn () => listeners.delete(fn)\n\t\t},\n\t\tstop: () => net.stop(),\n\t}\n}\n\nasync function join(net: VoleNetManager, hub: string): Promise<string> {\n\tconst res = await net.initiateJoin(hub)\n\tif (!res.ok) return `could not join ${hub}: ${res.error}`\n\tif (res.pending) return `waiting for approval at ${hub}`\n\treturn `joined ${res.hubName ?? hub}`\n}\n\n/**\n * Whether the hub at this URL is already trusted, so a restart does not re-join every time.\n * It asks who the hub says it is, then looks that id up in what we already trust.\n */\nexport async function alreadyTrusts(dir: string, hub: string): Promise<boolean> {\n\ttry {\n\t\tconst r = await fetch(`${hub.replace(/\\/$/, '')}/volenet/info`, {\n\t\t\tsignal: AbortSignal.timeout(8000),\n\t\t})\n\t\tconst info = (await r.json()) as { publicKey?: string }\n\t\tconst parsed = info.publicKey ? parsePublicKey(info.publicKey) : null\n\t\tif (!parsed) return false\n\t\treturn (await loadAuthorizedVoles(path.join(dir, 'net'))).has(parsed.instanceId)\n\t} catch {\n\t\treturn false\n\t}\n}\n","/**\n * The tools, and what they are for.\n *\n * Deliberately few. An MCP server's tool list is spent from the client's context window on every\n * turn, and the agent's whole tool registry — which is what `/mcp/<agent>` already exposes — is the\n * wrong shape here: this server is about the *network*, not about one agent's abilities. Eight\n * verbs cover it: who am I, who is out there, what did I miss, say something, read a thread, ask\n * an agent to think, and the two halves of deciding whom to trust.\n */\nimport { saveStored } from './config.js'\nimport { type Node, alreadyTrusts } from './node.js'\n\nexport interface ToolDef {\n\tname: string\n\tdescription: string\n\tinputSchema: Record<string, unknown>\n\trun: (node: Node, args: Record<string, unknown>) => Promise<string>\n}\n\nconst obj = (properties: Record<string, unknown>, required: string[] = []) => ({\n\ttype: 'object' as const,\n\tproperties,\n\t...(required.length ? { required } : {}),\n})\nconst str = (description: string) => ({ type: 'string' as const, description })\nconst num = (description: string) => ({ type: 'number' as const, description })\n\nconst when = (ts: number) => (ts ? new Date(ts).toISOString().replace('T', ' ').slice(0, 19) : '-')\n\n/** One peer, however we can reach it. */\ninterface Peer {\n\tid: string\n\tname: string\n\troute: 'direct' | 'hub'\n\tconnected: boolean\n\t/** Hub peers only: whether the consent handshake has completed both ways. */\n\tconsented?: boolean\n\tviaHub?: string\n}\n\nfunction peers(node: Node): Peer[] {\n\t// Whether a direct peer is live is the transport's business — an instance record persists\n\t// after the socket does, so `lastSeen` alone would report a dead link as online.\n\tconst live = new Set(\n\t\t(node.net.getTransport()?.getPeers() ?? []).filter((p) => p.connected).map((p) => p.peerId),\n\t)\n\tconst out: Peer[] = node.net.getInstances().map((i) => ({\n\t\tid: i.id,\n\t\tname: i.name,\n\t\troute: 'direct' as const,\n\t\tconnected: live.has(i.id),\n\t}))\n\tconst direct = new Set(out.map((p) => p.id))\n\tfor (const m of node.net.getRelayMembers()) {\n\t\tif (direct.has(m.id)) continue\n\t\tout.push({\n\t\t\tid: m.id,\n\t\t\tname: m.name,\n\t\t\troute: 'hub',\n\t\t\tconnected: m.connected,\n\t\t\tconsented: m.accepted,\n\t\t\tviaHub: m.viaHubName,\n\t\t})\n\t}\n\treturn out\n}\n\nfunction resolve(node: Node, ref: string): Peer | undefined {\n\tconst all = peers(node)\n\treturn (\n\t\tall.find((p) => p.id === ref) ??\n\t\tall.find((p) => p.name === ref) ??\n\t\tall.find((p) => p.id.startsWith(ref)) ??\n\t\tall.find((p) => p.name.toLowerCase() === ref.toLowerCase())\n\t)\n}\n\nexport const TOOLS: ToolDef[] = [\n\t{\n\t\tname: 'volenet_whoami',\n\t\tdescription:\n\t\t\t\"This session's own identity on the VoleNet mesh — name, instance id, hub, and whether the mesh is reachable. Call this first if you are unsure. Pass key:true only when someone actually needs the full public key; it is several kilobytes of post-quantum key material.\",\n\t\tinputSchema: obj({\n\t\t\tkey: { type: 'boolean' as const, description: 'Include the full public key string' },\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tconst key = node.net.getKeyPair()\n\t\t\tconst online = peers(node).filter((p) => p.connected).length\n\t\t\tconst lines = [\n\t\t\t\t`name ${node.options.name}`,\n\t\t\t\t`instanceId ${key?.instanceId ?? '(not started)'}`,\n\t\t\t\t`hub ${node.hubStatus}`,\n\t\t\t\t`connected ${online} peer(s) online`,\n\t\t\t\t`listening port ${node.options.port} (reachable only from networks that can dial it)`,\n\t\t\t\t`store ${node.options.dir}`,\n\t\t\t]\n\t\t\t// The hybrid key string is ~2.5 KB — most of an ML-DSA-65 key — and spending that on\n\t\t\t// every call would be a real cost to the session for something rarely needed.\n\t\t\tif (args.key) lines.push('', `publicKey ${key?.publicKeyString ?? '-'}`)\n\t\t\telse\n\t\t\t\tlines.push(\n\t\t\t\t\t'',\n\t\t\t\t\t'Public key withheld (large). Call again with key:true when a peer needs it.',\n\t\t\t\t)\n\t\t\tlines.push(\n\t\t\t\t'',\n\t\t\t\t'To be reachable by someone else: give them that public key to trust, or send them a',\n\t\t\t\t'pair request with volenet_connect and have their operator accept it.',\n\t\t\t)\n\t\t\treturn lines.join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_peers',\n\t\tdescription:\n\t\t\t'Everyone this session can reach: agents and people, whether the link is direct or through a hub, and whether they are online right now.',\n\t\tinputSchema: obj({}),\n\t\tasync run(node) {\n\t\t\tconst all = peers(node)\n\t\t\tif (all.length === 0) {\n\t\t\t\treturn 'No peers. Join a hub (VOLENET_MCP_HUB) or pair with a node directly (volenet_pair).'\n\t\t\t}\n\t\t\treturn all\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst bits = [\n\t\t\t\t\t\tp.connected ? 'online ' : 'away ',\n\t\t\t\t\t\tp.route === 'direct' ? 'direct' : `via ${p.viaHub}`,\n\t\t\t\t\t\tp.name,\n\t\t\t\t\t\tp.id.substring(0, 8),\n\t\t\t\t\t]\n\t\t\t\t\tif (p.route === 'hub' && !p.consented) bits.push('(no consent yet — volenet_connect)')\n\t\t\t\t\treturn ` ${bits.join(' ')}`\n\t\t\t\t})\n\t\t\t\t.join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_inbox',\n\t\tdescription:\n\t\t\t'Messages that arrived for this session, including while it was not running, plus who tried to reach it while it was away. Reading marks them seen. Check this at the start of a session.',\n\t\tinputSchema: obj({}),\n\t\tasync run(node) {\n\t\t\tconst unread = node.inbox.unread()\n\t\t\tconst lines: string[] = []\n\t\t\tif (unread.length === 0) lines.push('No new messages.')\n\t\t\telse {\n\t\t\t\tlines.push(`${unread.length} new message(s):`, '')\n\t\t\t\tfor (const m of unread) {\n\t\t\t\t\tlines.push(` [${when(m.ts)}] ${m.peerName} (${m.peerId.substring(0, 8)})`)\n\t\t\t\t\tlines.push(` ${m.text.replace(/\\n/g, '\\n ')}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (node.notices.length > 0) {\n\t\t\t\tlines.push('', 'Tried to reach you while you were away:')\n\t\t\t\tfor (const n of node.notices) {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t` ${n.fromName} (${n.from.substring(0, 8)}) — ${n.count}x, last ${when(n.last)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tlines.push(\n\t\t\t\t\t' Their messages are held on their own device and arrive when they are next online.',\n\t\t\t\t)\n\t\t\t}\n\t\t\tawait node.inbox.markRead()\n\t\t\treturn lines.join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_send',\n\t\tdescription:\n\t\t\t'Send a message to a person or an agent. This is chat: it is delivered and read by whoever is there, and does NOT run their brain or wait for a reply. Use this to reach a person on the phone app. If the peer is offline the message waits here and goes out when they return.',\n\t\tinputSchema: obj(\n\t\t\t{ to: str('Peer name or instance id (see volenet_peers)'), text: str('What to say') },\n\t\t\t['to', 'text'],\n\t\t),\n\t\tasync run(node, args) {\n\t\t\tconst to = String(args.to ?? '')\n\t\t\tconst text = String(args.text ?? '')\n\t\t\tif (!text.trim()) return 'Nothing to send.'\n\t\t\tconst peer = resolve(node, to)\n\t\t\tconst res = await node.net.sendChat(peer?.id ?? to, text)\n\t\t\tif (!res.ok) return `Not sent: ${res.error ?? 'unknown error'}`\n\t\t\tawait node.inbox.add({\n\t\t\t\tpeerId: peer?.id ?? to,\n\t\t\t\tpeerName: peer?.name ?? to,\n\t\t\t\tdir: 'out',\n\t\t\t\ttext,\n\t\t\t\tts: Date.now(),\n\t\t\t\tid: `out-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n\t\t\t})\n\t\t\tif (res.delivered === false) {\n\t\t\t\treturn `Held for ${peer?.name ?? to}: they are not reachable right now, and it goes out when they are back.`\n\t\t\t}\n\t\t\treturn `Sent to ${peer?.name ?? to}${res.relayed ? ' (through a hub, sealed end to end)' : ''}.`\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_history',\n\t\tdescription: 'The conversation with one peer, oldest first.',\n\t\tinputSchema: obj({\n\t\t\tpeer: str('Peer name or instance id'),\n\t\t\tlimit: num('Messages (default 50)'),\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tconst peer = resolve(node, String(args.peer ?? ''))\n\t\t\tconst id = peer?.id ?? String(args.peer ?? '')\n\t\t\tconst msgs = node.inbox.history(id, Number(args.limit ?? 50))\n\t\t\tif (msgs.length === 0) return `Nothing recorded with ${peer?.name ?? id}.`\n\t\t\treturn msgs\n\t\t\t\t.map((m) => `[${when(m.ts)}] ${m.dir === 'out' ? 'you' : m.peerName}: ${m.text}`)\n\t\t\t\t.join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_ask',\n\t\tdescription:\n\t\t\t\"Ask another AGENT's brain a question and wait for its answer. This runs the peer's model, so it takes as long as thinking takes, and the peer's operator must have granted this session brain access. Only works over a direct link — a hub will not carry it — and a person's phone app has no brain to ask; use volenet_send for people.\",\n\t\tinputSchema: obj(\n\t\t\t{\n\t\t\t\tto: str('Agent name or instance id'),\n\t\t\t\tquestion: str('What to ask'),\n\t\t\t\ttimeout_ms: num('How long to wait (default 120000)'),\n\t\t\t},\n\t\t\t['to', 'question'],\n\t\t),\n\t\tasync run(node, args) {\n\t\t\tconst peer = resolve(node, String(args.to ?? ''))\n\t\t\tif (!peer) return `No peer found: \"${args.to}\". Use volenet_peers.`\n\t\t\tif (peer.route !== 'direct') {\n\t\t\t\treturn `${peer.name} is only reachable through a hub, and a hub will not relay a question to an agent's brain — it carries chat and consent only. Use volenet_send, or pair directly with volenet_pair.`\n\t\t\t}\n\t\t\tconst mgr = node.net.getRemoteTaskManager()\n\t\t\tif (!mgr) return 'Remote task manager not available.'\n\t\t\tconst res = await mgr.delegateTask(\n\t\t\t\tpeer.id,\n\t\t\t\t{ taskId: '', input: String(args.question ?? ''), fromName: node.options.name },\n\t\t\t\tNumber(args.timeout_ms ?? 120_000),\n\t\t\t)\n\t\t\tif (res.status === 'completed') return `${peer.name} says:\\n\\n${res.result}`\n\t\t\tconst why = res.error ?? res.status\n\t\t\tif (typeof why === 'string' && why.includes('allowBrain')) {\n\t\t\t\treturn `${peer.name} refused: ${why}\\n\\nIts operator can allow this session by adding { \"id\": \"${node.net.getKeyPair()?.instanceId}\", \"trust\": \"read\", \"allowBrain\": true } to net.peers and restarting.`\n\t\t\t}\n\t\t\treturn `${peer.name} did not answer: ${why}`\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_requests',\n\t\tdescription:\n\t\t\t'Trust decisions waiting on you: nodes asking to be trusted, and hub members asking to chat. Accept or deny one by naming it. Nothing is trusted until you say so.',\n\t\tinputSchema: obj({\n\t\t\taccept: str('Name or id to accept'),\n\t\t\tdeny: str('Name or id to deny'),\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tconst accept = args.accept ? String(args.accept) : undefined\n\t\t\tconst deny = args.deny ? String(args.deny) : undefined\n\t\t\tconst act = accept ?? deny\n\t\t\tif (act) {\n\t\t\t\tconst req = node.requests.find((r) => r.fromName === act || r.from.startsWith(act))\n\t\t\t\tif (!req) return `No pending request matching \"${act}\".`\n\t\t\t\tconst ok = accept\n\t\t\t\t\t? req.kind === 'pair'\n\t\t\t\t\t\t? await node.net.acceptPair(req.from)\n\t\t\t\t\t\t: await node.net.approveRelayConnect(req.from)\n\t\t\t\t\t: req.kind === 'pair'\n\t\t\t\t\t\t? await node.net.denyPair(req.from)\n\t\t\t\t\t\t: await node.net.denyRelayConnect(req.from)\n\t\t\t\tnode.requests.splice(node.requests.indexOf(req), 1)\n\t\t\t\treturn ok.ok\n\t\t\t\t\t? `${accept ? 'Accepted' : 'Denied'} ${req.fromName}.`\n\t\t\t\t\t: `Failed: ${'error' in ok ? ok.error : 'unknown'}`\n\t\t\t}\n\t\t\tconst pairs = node.net.listPairRequests()\n\t\t\tfor (const p of pairs) {\n\t\t\t\tif (!node.requests.some((r) => r.from === p.id)) {\n\t\t\t\t\tnode.requests.push({ kind: 'pair', from: p.id, fromName: p.name, at: Date.now() })\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (node.requests.length === 0) return 'Nothing waiting.'\n\t\t\treturn node.requests\n\t\t\t\t.map(\n\t\t\t\t\t(r) =>\n\t\t\t\t\t\t` ${r.kind === 'pair' ? 'wants to be trusted' : 'wants to chat '} ${r.fromName} ${r.from.substring(0, 8)}${r.note ? ` — \"${r.note}\"` : ''}`,\n\t\t\t\t)\n\t\t\t\t.join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_wait',\n\t\tdescription:\n\t\t\t'Wait for the next message to arrive, instead of checking again later. Use it whenever you have said something and a reply is expected — it turns a mailbox into a conversation. Returns as soon as anything lands, or reports that nothing came within the time given. Nothing is lost either way: a message that arrives after you stop waiting is still in the inbox.',\n\t\tinputSchema: obj({\n\t\t\tfrom: str('Only wake for this peer, by name or id. Omit to wake for anyone.'),\n\t\t\ttimeout_ms: num('How long to wait. Default 60000, maximum 300000.'),\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tconst limit = Math.min(Math.max(Number(args.timeout_ms ?? 60_000), 1_000), 300_000)\n\t\t\tconst want = args.from ? resolve(node, String(args.from)) : undefined\n\t\t\tconst wanted = (m: { peerId: string }) => !args.from || m.peerId === (want?.id ?? args.from)\n\n\t\t\t// Anything already unread counts as arrived: waiting for the next one would skip it.\n\t\t\tconst already = node.inbox.unread().filter(wanted)\n\t\t\tif (already.length === 0) {\n\t\t\t\tawait new Promise<void>((done) => {\n\t\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\t\toff()\n\t\t\t\t\t\tdone()\n\t\t\t\t\t}, limit)\n\t\t\t\t\tconst off = node.onMessage((m) => {\n\t\t\t\t\t\tif (!wanted(m)) return\n\t\t\t\t\t\tclearTimeout(timer)\n\t\t\t\t\t\toff()\n\t\t\t\t\t\tdone()\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst arrived = node.inbox.unread().filter(wanted)\n\t\t\tawait node.inbox.markRead()\n\t\t\tif (arrived.length === 0) {\n\t\t\t\treturn `Nothing arrived within ${Math.round(limit / 1000)}s${args.from ? ` from ${want?.name ?? args.from}` : ''}. It is not lost — whatever they send lands in the inbox whenever it comes.`\n\t\t\t}\n\t\t\treturn arrived.map((m) => `[${when(m.ts)}] ${m.peerName}: ${m.text}`).join('\\n')\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_hub',\n\t\tdescription:\n\t\t\t'Join a hub, leave one, or report which hub this session is on. A hub makes you reachable by people and agents that cannot dial your machine — it carries sealed traffic it cannot read, and stores no message. Called with no arguments it just reports. The choice is remembered, so the next session starts where this one left off.',\n\t\tinputSchema: obj({\n\t\t\turl: str('Hub URL to join, e.g. https://hub.example.com/mesh'),\n\t\t\tleave: { type: 'boolean' as const, description: 'Leave the current hub' },\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tif (args.leave) {\n\t\t\t\tconst hub = node.options.hub\n\t\t\t\tif (!hub) return 'Not on a hub.'\n\t\t\t\t// Forget it and drop the socket, so neither this session nor the next dials it.\n\t\t\t\tnode.net.forgetPeer(hub)\n\t\t\t\tnode.options.hub = undefined\n\t\t\t\tnode.hubStatus = 'no hub configured'\n\t\t\t\tawait saveStored(node.options.dir, { hub: undefined })\n\t\t\t\treturn `Left ${hub}. Your identity and everyone you have paired with directly are untouched.`\n\t\t\t}\n\t\t\tif (!args.url) {\n\t\t\t\treturn node.options.hub\n\t\t\t\t\t? `${node.hubStatus}\\n\\nCall with leave:true to come off it, or url to move to another.`\n\t\t\t\t\t: 'Not on a hub. Give a url to join one — or stay off it and pair directly with volenet_connect.'\n\t\t\t}\n\t\t\tconst url = String(args.url).replace(/\\/$/, '')\n\t\t\t// Already trusted — a hub we have joined before, or paired with — so there is nothing to\n\t\t\t// introduce. Joining again would need its public join still open, which is not a thing\n\t\t\t// coming back should depend on.\n\t\t\tif (await alreadyTrusts(node.options.dir, url)) {\n\t\t\t\tawait node.net.addPeer(url)\n\t\t\t\tnode.options.hub = url\n\t\t\t\tnode.hubStatus = `joined ${url}`\n\t\t\t\tawait saveStored(node.options.dir, { hub: url })\n\t\t\t\treturn `Joined ${url} again — it was already trusted, so no introduction was needed.`\n\t\t\t}\n\t\t\tconst res = await node.net.initiateJoin(url)\n\t\t\tif (!res.ok) return `Could not join ${url}: ${res.error}`\n\t\t\tnode.options.hub = url\n\t\t\tnode.hubStatus = res.pending\n\t\t\t\t? `waiting for approval at ${url}`\n\t\t\t\t: `joined ${res.hubName ?? url}`\n\t\t\tawait saveStored(node.options.dir, { hub: url })\n\t\t\treturn res.pending\n\t\t\t\t? `Asked to join ${url}. Its operator has to approve before you appear in the roster.`\n\t\t\t\t: `Joined ${res.hubName ?? url}. Remembered, so the next session starts here. Call volenet_peers to see who is around.`\n\t\t},\n\t},\n\t{\n\t\tname: 'volenet_connect',\n\t\tdescription:\n\t\t\t'Reach out to someone new: pair directly with a node at a URL, or ask a hub member for consent to chat. Pairing is two calls — the first reports the fingerprint of whoever answers, the second confirms it — because trusting a URL blind is trusting whoever holds it. Neither side trusts you until they accept.',\n\t\tinputSchema: obj({\n\t\t\turl: str('Node URL to pair with directly, e.g. http://10.0.0.5:9700'),\n\t\t\tconfirm: str('The fingerprint returned by a first call with url, confirming who answers'),\n\t\t\tmember: str('Hub member name or id to ask for chat consent'),\n\t\t\tnote: str('A line saying who you are'),\n\t\t}),\n\t\tasync run(node, args) {\n\t\t\tconst note = args.note ? String(args.note) : undefined\n\t\t\tif (args.url) {\n\t\t\t\tconst url = String(args.url)\n\t\t\t\tconst probe = await node.net.probePair(url)\n\t\t\t\tif (!probe.ok || !probe.publicKey) return `Could not reach it: ${probe.error}`\n\t\t\t\t// Trust on first use is a decision, not a side effect: whoever answers that URL is\n\t\t\t\t// whoever answers that URL. Show the fingerprint and require it back before trusting.\n\t\t\t\tconst confirm = args.confirm ? String(args.confirm).trim() : ''\n\t\t\t\tif (!confirm) {\n\t\t\t\t\treturn [\n\t\t\t\t\t\t`${probe.name ?? url} answers with fingerprint:`,\n\t\t\t\t\t\t` ${probe.fingerprint}`,\n\t\t\t\t\t\tprobe.alreadyTrusted ? ' (already trusted by this session)' : '',\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'Check that against what the other side reports, then call this again with',\n\t\t\t\t\t\t'confirm set to that fingerprint to trust it and send the pair request.',\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join('\\n')\n\t\t\t\t}\n\t\t\t\tif (!probe.fingerprint?.startsWith(confirm)) {\n\t\t\t\t\treturn `That fingerprint does not match: it answers with ${probe.fingerprint}. Nothing was trusted.`\n\t\t\t\t}\n\t\t\t\tconst res = await node.net.initiatePair(url, probe.publicKey, note)\n\t\t\t\treturn res.ok\n\t\t\t\t\t? `Trusted ${probe.name ?? url} and asked it to trust this session. Nothing arrives until their operator accepts.`\n\t\t\t\t\t: `Could not ask: ${res.error}`\n\t\t\t}\n\t\t\tif (args.member) {\n\t\t\t\tconst res = await node.net.requestRelayConnect(String(args.member), note)\n\t\t\t\tif (!res.ok) return `Could not ask: ${res.error}`\n\t\t\t\treturn res.queued\n\t\t\t\t\t? `${args.member} is away — the request waits here and goes out when they are back.`\n\t\t\t\t\t: `Asked ${args.member} for consent to chat.`\n\t\t\t}\n\t\t\treturn 'Give either a url (direct pairing) or a member (hub consent).'\n\t\t},\n\t},\n]\n"],"mappings":"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","names":["path","fs","path","file","path","run","path","join","when"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@openvole/volenet-mcp",
3
+ "version": "0.1.0",
4
+ "description": "VoleNet as an MCP server — give Claude Code an identity on the mesh, so it can reach people and other agents across machines",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "volenet-mcp": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.30.0",
19
+ "@openvole/volenet": "1.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "tsup": "^8.3.0",
24
+ "typescript": "^5.6.0",
25
+ "vitest": "^2.1.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "license": "MIT",
35
+ "homepage": "https://openvole.com/openvole/volenet",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/openvole/openvole",
39
+ "directory": "src/volenet-mcp"
40
+ },
41
+ "keywords": [
42
+ "openvole",
43
+ "volenet",
44
+ "mcp",
45
+ "claude-code",
46
+ "agent"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }