sigelo-mcp 0.1.5 → 0.1.6
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/contact.mjs +14 -7
- package/package.json +4 -4
- package/server.mjs +50 -11
package/contact.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const int = (d, maximum) => ({ type: 'integer', minimum: 0, ...(maximum !== unde
|
|
|
27
27
|
// which binds both original DIDs) are what two operators read to each other to know their agents
|
|
28
28
|
// paired with each other, as sigelo-agent's accept, inbox and contacts show them. 44 bits: the
|
|
29
29
|
// 32-digit pair id stays beside them for whoever wants all 128 (THREAT-MODEL.md §2.10).
|
|
30
|
-
const CONFIRM = 'Show your operator the contact\'s DID and
|
|
30
|
+
const CONFIRM = 'Show your operator the contact\'s DID and four words (fingerprint) to read to the other side. Same words = no mix-up or casual swap; a relay spending GPU time is caught by pair (32 digits).';
|
|
31
31
|
// The proof of work runs at an identity's first login: about 23 s on average at 22 bits on a
|
|
32
32
|
// phone-class CPU, 92 s at 24 (design/relay-plan.md §11), past some clients' tool timeouts.
|
|
33
33
|
const POW = 'A first call can take minutes: your first login to the relay solves a proof of work. If your client gives up, call again.';
|
|
@@ -39,7 +39,7 @@ const TOOLS = [
|
|
|
39
39
|
obj({ name: str('the name you will call the new contact'), ttl: int('seconds the long code lives, at most 604800 (7 days)'), long: { type: 'boolean', description: 'true: the long code only, no short code' } })],
|
|
40
40
|
['sigelo_accept', `Accept an invite code from another agent's operator: sends your answer (whoever holds the code learns your DID); pairs once the inviter's reply verifies against the code. Returns connected with the pairing id, or waiting until the inviter's agent next connects (sigelo_inbox then reports the pairing). ${CONFIRM} ${POW} ${RELAY_DATA}`,
|
|
41
41
|
obj({ code: str('the short code (K7F3-Q9ZP; case, I/L/O and dashes do not matter) or the sgl1… code, exactly as received'), name: str('the name you will call this contact'), wait: int(`seconds to wait for the reply, default 10, at most ${WAIT_MAX}`, WAIT_MAX) }, ['code'])],
|
|
42
|
-
['sigelo_contacts', `Your contacts (name, original DID, current DID, revoked, relay, messages queued for them, pairing id, four words, re_pair)
|
|
42
|
+
['sigelo_contacts', `Your contacts (name, original DID, current DID, revoked, relay, messages queued for them, pairing id, four words, re_pair), your pending invites and answers, and failed pairings (code_expired: ask for a new code). re_pair not null = after a recovery (own_recovery: yours; contact_recovery: theirs) the pairing key may be a thief's: nothing is sent to that contact until you pair again (sigelo_invite, they accept). ${CONFIRM}`, obj()],
|
|
43
43
|
['sigelo_send', `Send an end-to-end encrypted message to a contact, by name or DID. The relay sees only ciphertext. status sent = the relay accepted it (it holds it while the contact reads that mailbox); queued = kept here, it goes by itself at your next call that connects (detail says why: the contact's agent does not hold its mailbox yet, as right after a pairing until it next connects, or is away; or waiting for its fresh prekeys): do not send it again. Error not_paired = your answer to its code waits for the inviter's agent. Error re_pair = pair again first (see sigelo_contacts). ${RELAY_DATA}`,
|
|
44
44
|
obj({ to: str('contact name or DID'), text: str('the message'), label: str('optional conversation label, e.g. "billing"') }, ['to', 'text'])],
|
|
45
45
|
['sigelo_inbox', `Messages from your contacts since you last asked (each returned once), and events: pairings done (with the four words to show your operator) or failed (with why), contacts' key rotations, re_pair (pair again after a recovery), refused (your queued messages to such a contact; or a contact's messages signed by a key its recovery overruled, maybe a thief's), waiting (for a contact's prekeys or its agent). last_resort = weaker forward secrecy. dropped: refusals by check. more > 0: call again now. ${DATA} ${RELAY_DATA}`,
|
|
@@ -56,7 +56,7 @@ const TOOLS = [
|
|
|
56
56
|
// keeps the held inbox. → null, with one line on stderr, when sigelo-contact (in that install) or
|
|
57
57
|
// one of its dependencies is not installed. Anything else (a refused package, a file missing in
|
|
58
58
|
// one, a syntax error) throws: a broken install must not pass for an absent one.
|
|
59
|
-
export async function load({ installed, id, env = process.env, clock }) {
|
|
59
|
+
export async function load({ installed, id, env = process.env, clock, debug = () => {} }) {
|
|
60
60
|
const held = await import(installed ? 'sigelo-agent/dist/sigelo-agent-contact.js' : '../../adapters/moadim/sigelo-agent-contact.ts');
|
|
61
61
|
let lib, kdf;
|
|
62
62
|
try { ({ lib, kdf } = await held.contactLib()); } catch (e) {
|
|
@@ -86,10 +86,12 @@ export async function load({ installed, id, env = process.env, clock }) {
|
|
|
86
86
|
queues.set(path, run.catch(() => {}));
|
|
87
87
|
return run;
|
|
88
88
|
};
|
|
89
|
-
|
|
89
|
+
// A refusal of the call's arguments: code usage (server.mjs codeOf reads .check).
|
|
90
|
+
const usage = (why) => Object.assign(new Error(why), { check: 'usage' });
|
|
91
|
+
const text = (v, name) => { if (typeof v !== 'string' || v === '') throw usage(`${name} is required`); return v; };
|
|
90
92
|
const seconds = (v, name, dflt) => {
|
|
91
93
|
if (v === undefined) return dflt;
|
|
92
|
-
if (!Number.isSafeInteger(v) || v < 0 || v > WAIT_MAX) throw
|
|
94
|
+
if (!Number.isSafeInteger(v) || v < 0 || v > WAIT_MAX) throw usage(`${name}: whole seconds, 0 to ${WAIT_MAX}`);
|
|
93
95
|
return v;
|
|
94
96
|
};
|
|
95
97
|
// The pairing id of a live contact, from the pairing key in the contact store (store.mjs); null
|
|
@@ -116,13 +118,14 @@ export async function load({ installed, id, env = process.env, clock }) {
|
|
|
116
118
|
const inbox = async ({ wait }, path) => {
|
|
117
119
|
const w = seconds(wait, 'wait', 0), file = held.heldPath(path);
|
|
118
120
|
const fresh = held.peekHeld(file).messages.length > 0 ? { messages: [], events: [], dropped: {} } : await carrier(path).inbox({ wait: w });
|
|
121
|
+
if (fresh.offline !== undefined) debug(carrier(path).lastError, path);
|
|
119
122
|
const { text, rest } = page(held.merge(held.takeHeld(file), fresh));
|
|
120
123
|
held.putBack(file, { messages: rest, events: [], dropped: {} });
|
|
121
124
|
return { text, isError: false };
|
|
122
125
|
};
|
|
123
126
|
const handlers = {
|
|
124
127
|
sigelo_invite: serial(({ name, ttl, long }, path) => {
|
|
125
|
-
if (long !== undefined && typeof long !== 'boolean') throw
|
|
128
|
+
if (long !== undefined && typeof long !== 'boolean') throw usage('long: true or false');
|
|
126
129
|
return carrier(path).invite({ name, ttl, short: long !== true });
|
|
127
130
|
}),
|
|
128
131
|
// A code never contains whitespace; one pasted from a wrapped mail line may, and a short one
|
|
@@ -134,7 +137,11 @@ export async function load({ installed, id, env = process.env, clock }) {
|
|
|
134
137
|
sigelo_contacts: (_, path) => { const r = carrier(path).contacts(); return { ...r, contacts: r.contacts.map((c) => ({ ...c, pair: pairOf(path, c.did) })) }; },
|
|
135
138
|
sigelo_send: serial(({ to, text: t, label }, path) => carrier(path).send(text(to, 'to'), text(t, 'text'), { label: label ?? '' })),
|
|
136
139
|
sigelo_inbox: serial(inbox),
|
|
137
|
-
sigelo_forget_contact: serial(({ did }, path) =>
|
|
140
|
+
sigelo_forget_contact: serial(async ({ did }, path) => {
|
|
141
|
+
const r = await carrier(path).forget(text(did, 'did'));
|
|
142
|
+
if (r.offline !== undefined) debug(carrier(path).lastError, path);
|
|
143
|
+
return r;
|
|
144
|
+
}),
|
|
138
145
|
};
|
|
139
146
|
return { tools: TOOLS, handlers };
|
|
140
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigelo-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"mcpName": "io.github.csigelo/sigelo",
|
|
5
5
|
"description": "MCP server: portable, offline-verifiable identity for AI agents (DID, challenges, attestations, bundles), optional Monero wallet",
|
|
6
6
|
"keywords": [
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"prepublishOnly": "node -e \"if(process.env.SIGELO_PUBLISH!=='1'){console.error('refusing: the npm account and names are an Owner decision (ROADMAP D1); publish the release/build.sh tarballs with SIGELO_PUBLISH=1');process.exit(1)}\""
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"sigelo": "^0.1.
|
|
47
|
-
"sigelo-agent": "^0.1.
|
|
48
|
-
"sigelo-spend": "^0.1.
|
|
46
|
+
"sigelo": "^0.1.6",
|
|
47
|
+
"sigelo-agent": "^0.1.6",
|
|
48
|
+
"sigelo-spend": "^0.1.6"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/server.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// over the agent-side library in adapters/moadim and, when a keeper is configured, the
|
|
6
6
|
// `sigelo-wallet` client in spend/. Zero dependencies of its own: no crypto here, only calls.
|
|
7
7
|
// Transport: one JSON-RPC message per line on stdin/stdout; logs go to stderr only.
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
// Installed (npm), the libraries are the packages sigelo, sigelo-agent and sigelo-spend; run
|
|
11
12
|
// from a clone of the repository, they are the sibling directories, and the agent library runs
|
|
@@ -25,7 +26,7 @@ console.log = console.info = console.debug = console.error;
|
|
|
25
26
|
|
|
26
27
|
const MODERN = ['2026-07-28']; // per-request _meta era
|
|
27
28
|
const LEGACY = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']; // initialize era
|
|
28
|
-
const SERVER_INFO = { name: 'sigelo', version: '0.1.
|
|
29
|
+
const SERVER_INFO = { name: 'sigelo', version: '0.1.6' };
|
|
29
30
|
const PV = 'io.modelcontextprotocol/protocolVersion';
|
|
30
31
|
const env = process.env;
|
|
31
32
|
const WALLET = Boolean(env.SIGELO_WALLET_URL && env.SIGELO_WALLET_TOKEN);
|
|
@@ -61,20 +62,52 @@ if (WALLET) TOOLS.push(
|
|
|
61
62
|
);
|
|
62
63
|
|
|
63
64
|
const base = id.identityPath();
|
|
65
|
+
// A refusal the framework can branch on: .check is its stable code (the contact library's MsgError
|
|
66
|
+
// carries one already), the message its one human line.
|
|
67
|
+
const refusal = (check, message, cause) => Object.assign(new SigeloError(message, cause === undefined ? undefined : { cause }), { check });
|
|
64
68
|
const pathOf = (name) => {
|
|
65
69
|
if (name === undefined || name === '') return base;
|
|
66
|
-
if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name)) throw
|
|
70
|
+
if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name)) throw refusal('usage', `identity: ${JSON.stringify(name)} is not a profile name; it must match [a-z0-9][a-z0-9_-]{0,63}, e.g. "researcher-1"`);
|
|
67
71
|
return join(dirname(base), `sigelo.${name}.local.json`);
|
|
68
72
|
};
|
|
73
|
+
// The profiles beside the default identity file, for a refusal naming one that is not there.
|
|
74
|
+
const profiles = () => {
|
|
75
|
+
try {
|
|
76
|
+
return readdirSync(dirname(base)).map((f) => (f === basename(base) ? '(default)' : /^sigelo\.([a-z0-9][a-z0-9_-]{0,63})\.local\.json$/.exec(f)?.[1])).filter(Boolean);
|
|
77
|
+
} catch { return []; }
|
|
78
|
+
};
|
|
69
79
|
const nowS = () => Math.floor(Date.now() / 1000);
|
|
70
80
|
const input = (v, name) => {
|
|
71
|
-
if (v === undefined || v === null) throw
|
|
72
|
-
|
|
81
|
+
if (v === undefined || v === null) throw refusal('usage', `${name} is required`);
|
|
82
|
+
// text goes through sigelo's strict parser (dup keys rejected)
|
|
83
|
+
if (typeof v === 'string') { try { return parse(v); } catch (e) { throw refusal('json', `${name}: not strict JSON (${e.message})`, e); } }
|
|
84
|
+
return v;
|
|
73
85
|
};
|
|
74
|
-
const update = (path, fn) => withLock(path, () => { const { store, result } = fn(id.load(path)); id.save(path, store); return result; });
|
|
75
86
|
const loadStore = (path) => {
|
|
76
|
-
|
|
87
|
+
if (!existsSync(path)) {
|
|
88
|
+
const named = path === base ? '' : ` for the profile "${/^sigelo\.(.+)\.local\.json$/.exec(path.slice(dirname(path).length + 1))?.[1]}"`, have = profiles();
|
|
89
|
+
throw refusal('no_identity', `no identity${named} at ${path}: call sigelo_init once${named ? ' with the same identity' : ''}${have.length ? `, or use an existing profile: ${have.join(', ')}` : ''}`);
|
|
90
|
+
}
|
|
91
|
+
// There, but not loading: never "call sigelo_init", which would make another identity.
|
|
92
|
+
try { return id.load(path); } catch (e) { throw refusal('identity_unreadable', `identity file ${path} does not load (${e.message.split('\n')[0]}): restore it from your copy; do not call sigelo_init, which makes a new identity`, e); }
|
|
77
93
|
};
|
|
94
|
+
const update = (path, fn) => withLock(path, () => { const { store, result } = fn(loadStore(path)); id.save(path, store); return result; });
|
|
95
|
+
// The stable code of any error a tool throws: its check, else by its class.
|
|
96
|
+
const codeOf = (e) => (typeof e?.check === 'string' ? e.check
|
|
97
|
+
: { SigeloError: 'refused', MoneroError: 'refused', JcsError: 'json' }[e?.name] ?? (typeof e?.code === 'string' && /^E[A-Z]+$/.test(e.code) ? 'io' : 'error'));
|
|
98
|
+
/** SIGELO_DEBUG=1: what is behind a refusal, on stderr (stdout is the protocol): the identity file,
|
|
99
|
+
* the relay settings, the cause chain with the relay's HTTP answers, the stack. */
|
|
100
|
+
function debug(e, path) {
|
|
101
|
+
if (env.SIGELO_DEBUG !== '1') return;
|
|
102
|
+
const say = (s) => process.stderr.write(`sigelo-mcp: debug: ${s}\n`);
|
|
103
|
+
if (path) say(`identity file ${path}${existsSync(path) ? '' : ', missing'}`);
|
|
104
|
+
say(`relay ${env.SIGELO_RELAY ?? '(default)'}${env.SIGELO_RELAY_ENDPOINTS ? ` endpoints ${env.SIGELO_RELAY_ENDPOINTS}` : ''}`);
|
|
105
|
+
for (let x = e, i = 0; x && i < 8; x = x.cause, i++) {
|
|
106
|
+
say(`${i ? 'caused by ' : 'error '}${x.name ?? typeof x}${x.check ? ` check=${x.check}` : ''}: ${x.message ?? String(x)}`);
|
|
107
|
+
if (x.http) say(`relay answer ${JSON.stringify(x.http)}`);
|
|
108
|
+
}
|
|
109
|
+
if (e?.stack) say(e.stack);
|
|
110
|
+
}
|
|
78
111
|
|
|
79
112
|
async function wallet(argv) {
|
|
80
113
|
const w = await lib('sigelo-spend/dist/wallet.js', '../../spend/dist/wallet.js');
|
|
@@ -85,7 +118,7 @@ async function wallet(argv) {
|
|
|
85
118
|
const HANDLERS = {
|
|
86
119
|
sigelo_whoami: (_, path) => { const s = loadStore(path); return { did: did(id.head(s)), genesis: id.head(s), chain: id.chainOf(s), attestations: s.attestations.length, identity_file: path }; },
|
|
87
120
|
sigelo_init: ({ recovery }, path) => {
|
|
88
|
-
if (typeof recovery !== 'string' || !recovery) throw
|
|
121
|
+
if (typeof recovery !== 'string' || !recovery) throw refusal('usage', 'recovery is required: a "z…" public key or "none"');
|
|
89
122
|
const s = id.init(path, recovery === 'none' ? null : recovery);
|
|
90
123
|
return { did: did(s.genesis), genesis: s.genesis, identity_file: path,
|
|
91
124
|
...(recovery === 'none' && { warning: 'NO recovery key: if this key is stolen the identity is lost permanently.' }) };
|
|
@@ -117,18 +150,24 @@ const HANDLERS = {
|
|
|
117
150
|
// otherwise, with one line on stderr, and a package that ships no contact.mjs gets null too. This
|
|
118
151
|
// server never depends on them. Anything else wrong in them stops it: a broken install must not
|
|
119
152
|
// pass for an absent one.
|
|
120
|
-
const contact = await import('./contact.mjs').then((m) => m.load({ installed: INSTALLED, id }), (e) => { if (e.code === 'ERR_MODULE_NOT_FOUND') return null; throw e; });
|
|
153
|
+
const contact = await import('./contact.mjs').then((m) => m.load({ installed: INSTALLED, id, debug }), (e) => { if (e.code === 'ERR_MODULE_NOT_FOUND') return null; throw e; });
|
|
121
154
|
if (contact) { TOOLS.push(...contact.tools); Object.assign(HANDLERS, contact.handlers); }
|
|
122
155
|
|
|
123
156
|
async function callTool(name, args) {
|
|
124
157
|
if (!TOOLS.some(([n]) => n === name)) return null;
|
|
158
|
+
let path;
|
|
125
159
|
try {
|
|
126
160
|
const a = args ?? {};
|
|
127
|
-
|
|
161
|
+
path = name.startsWith('sigelo_wallet_') || name === 'sigelo_verify' ? undefined : pathOf(a.identity);
|
|
162
|
+
const r = await HANDLERS[name](a, path);
|
|
128
163
|
if (r && typeof r.text === 'string' && 'isError' in r) return { content: [{ type: 'text', text: r.text }], structuredContent: r.structured, isError: r.isError };
|
|
129
164
|
return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }], isError: false };
|
|
130
165
|
} catch (e) {
|
|
131
|
-
|
|
166
|
+
// The message, never the stack (SIGELO_DEBUG=1 puts that on stderr); code: stable, to branch on.
|
|
167
|
+
const message = e instanceof Error ? e.message : String(e), code = codeOf(e);
|
|
168
|
+
debug(e, path);
|
|
169
|
+
// long: the long code a spent short code stood for (contact/carrier.mjs keepLong): keep it.
|
|
170
|
+
return { content: [{ type: 'text', text: `REFUSED: ${message}` }], structuredContent: { code, message, ...(typeof e?.long === 'string' && { long: e.long }) }, isError: true };
|
|
132
171
|
}
|
|
133
172
|
}
|
|
134
173
|
|