@adapt-toolkit/a2adapt 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/cli.js +2 -2
- package/dist/hooks/runner.js +22 -33
- package/dist/index.js +124 -53
- package/dist/mufl_code/CBC04E71F18E1EC2829E7645BB7F22BBBEEB775C359C062703B7A6FCFB60CFE2.muflo +0 -0
- package/dist/mufl_code/actor.mu +234 -30
- package/package.json +1 -1
- package/dist/mufl_code/A93F52302D67D1F28269D3F35657610A0FA140AABC521D667A262C101A7AC090.muflo +0 -0
package/README.md
CHANGED
|
@@ -6,14 +6,15 @@ agent-to-agent messaging tools over stdio. Distributed as part of the
|
|
|
6
6
|
|
|
7
7
|
The server **is** the node: on startup it boots a single ADAPT packet (a MUFL
|
|
8
8
|
messenger), restores prior state from the state dir, connects to the broker, and
|
|
9
|
-
exposes
|
|
9
|
+
exposes the messaging tools — each a thin wrapper over one MUFL user transaction:
|
|
10
10
|
|
|
11
11
|
- `generate_invite` — named invite to share out-of-band
|
|
12
12
|
- `add_contact` — add a contact from an invite blob (TOFU)
|
|
13
13
|
- `list_contacts`
|
|
14
14
|
- `send_message` — end-to-end encrypted
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
15
|
+
- `get_messages` — return unread messages (bodies) + mark read; delivered exactly once
|
|
16
|
+
- `mark_processed` / `defer_messages` — remove handled messages, or re-queue read ones for another session
|
|
17
|
+
- `list_incoming_messages` — full inbox with ids + status (read-only)
|
|
17
18
|
|
|
18
19
|
## Configuration
|
|
19
20
|
|
package/dist/cli.js
CHANGED
|
@@ -161,7 +161,7 @@ function cmdWatch(which) {
|
|
|
161
161
|
}
|
|
162
162
|
for (const name of names) {
|
|
163
163
|
if (which && name !== which) continue;
|
|
164
|
-
const logPath = join(STATE_DIR, name, "
|
|
164
|
+
const logPath = join(STATE_DIR, name, "notifications.log");
|
|
165
165
|
let size;
|
|
166
166
|
try {
|
|
167
167
|
size = fs.statSync(logPath).size;
|
|
@@ -198,7 +198,7 @@ function cmdWatch(which) {
|
|
|
198
198
|
continue;
|
|
199
199
|
}
|
|
200
200
|
out(
|
|
201
|
-
`[${name}] new message from ${msg.
|
|
201
|
+
`[${name}] new message from ${msg.from ?? "?"}` + (msg.msg_id !== void 0 ? ` (#${msg.msg_id})` : "") + (msg.date ? ` (${msg.date})` : "")
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
}
|
package/dist/hooks/runner.js
CHANGED
|
@@ -21,34 +21,26 @@ function emit(payload) {
|
|
|
21
21
|
function noop() {
|
|
22
22
|
emit({ continue: true });
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function readUnreadSnapshot(dir) {
|
|
25
|
+
let raw;
|
|
25
26
|
try {
|
|
26
|
-
|
|
27
|
+
raw = fs.readFileSync(join(dir, "unread.json"), "utf8");
|
|
27
28
|
} catch {
|
|
28
|
-
return
|
|
29
|
+
return null;
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
|
-
function readInboxLog(dir) {
|
|
32
|
-
let raw;
|
|
33
31
|
try {
|
|
34
|
-
|
|
32
|
+
const snap = JSON.parse(raw);
|
|
33
|
+
const count = Number(snap.count ?? 0);
|
|
34
|
+
if (!count) return null;
|
|
35
|
+
const recent = Array.isArray(snap.recent) ? snap.recent.map((m) => ({
|
|
36
|
+
from: String(m.from ?? "?"),
|
|
37
|
+
msg_id: m.msg_id ?? "?",
|
|
38
|
+
date: String(m.date ?? "")
|
|
39
|
+
})) : [];
|
|
40
|
+
return { name: "", count, recent };
|
|
35
41
|
} catch {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
const msgs = [];
|
|
39
|
-
for (const line of raw.split("\n")) {
|
|
40
|
-
if (!line.trim()) continue;
|
|
41
|
-
try {
|
|
42
|
-
const m = JSON.parse(line);
|
|
43
|
-
msgs.push({
|
|
44
|
-
sender: String(m.sender ?? "?"),
|
|
45
|
-
text: String(m.text ?? ""),
|
|
46
|
-
date: String(m.date ?? "")
|
|
47
|
-
});
|
|
48
|
-
} catch {
|
|
49
|
-
}
|
|
42
|
+
return null;
|
|
50
43
|
}
|
|
51
|
-
return msgs;
|
|
52
44
|
}
|
|
53
45
|
function collectUnread() {
|
|
54
46
|
let names;
|
|
@@ -59,12 +51,9 @@ function collectUnread() {
|
|
|
59
51
|
}
|
|
60
52
|
const out = [];
|
|
61
53
|
for (const name of names) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const unread = all.slice(readCursor(dir));
|
|
66
|
-
if (unread.length === 0) continue;
|
|
67
|
-
out.push({ name, count: unread.length, messages: unread });
|
|
54
|
+
const snap = readUnreadSnapshot(join(STATE_DIR, name));
|
|
55
|
+
if (!snap) continue;
|
|
56
|
+
out.push({ ...snap, name });
|
|
68
57
|
}
|
|
69
58
|
return out;
|
|
70
59
|
}
|
|
@@ -73,15 +62,15 @@ function renderContext(unread) {
|
|
|
73
62
|
const lines = [];
|
|
74
63
|
for (const u of unread) {
|
|
75
64
|
lines.push(`\u2022 ${u.name} \u2014 ${u.count} unread:`);
|
|
76
|
-
for (const m of u.
|
|
77
|
-
lines.push(`
|
|
65
|
+
for (const m of u.recent.slice(-5)) {
|
|
66
|
+
lines.push(` from ${m.from} (#${m.msg_id})${m.date ? ` (${m.date})` : ""}`);
|
|
78
67
|
}
|
|
79
|
-
if (u.count >
|
|
68
|
+
if (u.count > u.recent.length) lines.push(` \u2026and ${u.count - u.recent.length} earlier`);
|
|
80
69
|
}
|
|
81
|
-
return `a2adapt \u2014 ${total} unread message(s) across ${unread.length} identit${unread.length === 1 ? "y" : "ies"} (arrived while you were away):
|
|
70
|
+
return `a2adapt \u2014 ${total} unread message(s) across ${unread.length} identit${unread.length === 1 ? "y" : "ies"} (arrived while you were away; senders shown, bodies stay in the packet):
|
|
82
71
|
${lines.join("\n")}
|
|
83
72
|
|
|
84
|
-
To read
|
|
73
|
+
To read: choose_identity({ name }) then get_messages() (returns the bodies and marks them read). To wait for live replies, arm a Monitor on the per-identity wake source \`a2adapt-mcp watch <name>\` (each new-mail line wakes you).`;
|
|
85
74
|
}
|
|
86
75
|
function sessionStart() {
|
|
87
76
|
const raw = readStdin();
|
package/dist/index.js
CHANGED
|
@@ -22433,10 +22433,11 @@ import { fileURLToPath } from "node:url";
|
|
|
22433
22433
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
22434
22434
|
import { createServer as createHttpServer } from "node:http";
|
|
22435
22435
|
import * as fs from "node:fs";
|
|
22436
|
+
import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from "node:zlib";
|
|
22436
22437
|
import { adapt_wrapper } from "@adapt-toolkit/sdk/executables";
|
|
22437
22438
|
import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
22438
22439
|
import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
|
|
22439
|
-
var VERSION = true ? "0.
|
|
22440
|
+
var VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
22440
22441
|
var STATE_DIR = resolve(
|
|
22441
22442
|
process.env.A2ADAPT_STATE_DIR ?? resolve(homedir(), ".a2adapt")
|
|
22442
22443
|
);
|
|
@@ -22481,8 +22482,8 @@ var evictedSessions = /* @__PURE__ */ new Set();
|
|
|
22481
22482
|
var identityDir = (name) => join(STATE_DIR, name);
|
|
22482
22483
|
var seedPath = (dir) => join(dir, "identity.seed");
|
|
22483
22484
|
var dataPath = (dir) => join(dir, "state_data.bin");
|
|
22484
|
-
var
|
|
22485
|
-
var
|
|
22485
|
+
var notifyLogPath = (dir) => join(dir, "notifications.log");
|
|
22486
|
+
var unreadPath = (dir) => join(dir, "unread.json");
|
|
22486
22487
|
function listPersistedNames() {
|
|
22487
22488
|
if (!fs.existsSync(STATE_DIR)) return [];
|
|
22488
22489
|
return fs.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs.existsSync(seedPath(join(STATE_DIR, d.name)))).map((d) => d.name);
|
|
@@ -22508,43 +22509,47 @@ function saveState(id) {
|
|
|
22508
22509
|
log(`[${id.name}] failed to save state:`, String(err));
|
|
22509
22510
|
}
|
|
22510
22511
|
}
|
|
22511
|
-
function
|
|
22512
|
+
function appendNotifyLog(id, from, msgId, date3) {
|
|
22512
22513
|
try {
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
}
|
|
22517
|
-
}
|
|
22518
|
-
function writeCursor(dir, n) {
|
|
22519
|
-
try {
|
|
22520
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
22521
|
-
fs.writeFileSync(cursorPath(dir), String(n));
|
|
22522
|
-
} catch {
|
|
22514
|
+
fs.mkdirSync(id.dir, { recursive: true });
|
|
22515
|
+
const line = JSON.stringify({ event: "message_received", from, msg_id: msgId, date: date3 }) + "\n";
|
|
22516
|
+
fs.appendFileSync(notifyLogPath(id.dir), line);
|
|
22517
|
+
} catch (err) {
|
|
22518
|
+
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
22523
22519
|
}
|
|
22524
22520
|
}
|
|
22525
|
-
function
|
|
22521
|
+
function refreshUnread(id) {
|
|
22526
22522
|
try {
|
|
22523
|
+
const inbox = renderInbox(readonlyTx(id, "::actor::list_incoming_messages"));
|
|
22524
|
+
const unread = inbox.filter((m) => m.status === "unread");
|
|
22525
|
+
const snapshot = {
|
|
22526
|
+
count: unread.length,
|
|
22527
|
+
recent: unread.slice(-10).map((m) => ({ from: m.sender_name, msg_id: m.msg_id, date: m.date }))
|
|
22528
|
+
};
|
|
22527
22529
|
fs.mkdirSync(id.dir, { recursive: true });
|
|
22528
|
-
const
|
|
22529
|
-
fs.
|
|
22530
|
+
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
22531
|
+
fs.writeFileSync(tmp, JSON.stringify(snapshot));
|
|
22532
|
+
fs.renameSync(tmp, unreadPath(id.dir));
|
|
22530
22533
|
} catch (err) {
|
|
22531
|
-
log(`[${id.name}] failed to
|
|
22534
|
+
log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
|
|
22532
22535
|
}
|
|
22533
22536
|
}
|
|
22534
|
-
var
|
|
22537
|
+
var serversBySession = /* @__PURE__ */ new Map();
|
|
22535
22538
|
var inboxResourceUri = (name) => `a2adapt://inbox/${encodeURIComponent(name)}`;
|
|
22536
22539
|
function pushNotification(identityName, summary) {
|
|
22537
22540
|
log(`[${identityName}] notify:`, summary);
|
|
22538
|
-
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
|
|
22542
|
-
|
|
22543
|
-
}
|
|
22544
|
-
|
|
22545
|
-
|
|
22546
|
-
|
|
22547
|
-
|
|
22541
|
+
const sid = bindingOwner.get(identityName);
|
|
22542
|
+
if (!sid) return;
|
|
22543
|
+
const server = serversBySession.get(sid);
|
|
22544
|
+
if (!server) return;
|
|
22545
|
+
try {
|
|
22546
|
+
server.sendLoggingMessage({ level: "info", logger: "a2adapt", data: summary });
|
|
22547
|
+
} catch (e) {
|
|
22548
|
+
log("sendLoggingMessage failed:", String(e));
|
|
22549
|
+
}
|
|
22550
|
+
try {
|
|
22551
|
+
server.server.sendResourceUpdated({ uri: inboxResourceUri(identityName) });
|
|
22552
|
+
} catch {
|
|
22548
22553
|
}
|
|
22549
22554
|
}
|
|
22550
22555
|
function readonlyTx(id, name) {
|
|
@@ -22590,11 +22595,12 @@ function wireHandlers(id) {
|
|
|
22590
22595
|
const event = payload.Reduce("event").Visualize();
|
|
22591
22596
|
if (event === "message_received") {
|
|
22592
22597
|
const sender = payload.Reduce("sender_name").Visualize();
|
|
22593
|
-
const
|
|
22598
|
+
const msgId = payload.Reduce("msg_id").Visualize();
|
|
22594
22599
|
const date3 = payload.Reduce("date").Visualize();
|
|
22595
|
-
|
|
22600
|
+
appendNotifyLog(id, sender, msgId, date3);
|
|
22601
|
+
refreshUnread(id);
|
|
22596
22602
|
process.nextTick(
|
|
22597
|
-
() => pushNotification(id.name, `[${id.name}] new message from ${sender}
|
|
22603
|
+
() => pushNotification(id.name, `[${id.name}] new message from ${sender} (#${msgId})`)
|
|
22598
22604
|
);
|
|
22599
22605
|
} else if (event === "contact_accepted") {
|
|
22600
22606
|
const name = payload.Reduce("name").Visualize();
|
|
@@ -22680,6 +22686,7 @@ async function restoreIdentity(name) {
|
|
|
22680
22686
|
log(`[${name}] failed to import saved state (continuing fresh):`, String(err));
|
|
22681
22687
|
}
|
|
22682
22688
|
}
|
|
22689
|
+
refreshUnread(id);
|
|
22683
22690
|
return id;
|
|
22684
22691
|
}
|
|
22685
22692
|
async function bootWrapper() {
|
|
@@ -22758,10 +22765,12 @@ function renderInbox(v) {
|
|
|
22758
22765
|
const m = v.Reduce(i);
|
|
22759
22766
|
if (m.IsNil()) break;
|
|
22760
22767
|
out.push({
|
|
22768
|
+
msg_id: parseInt(m.Reduce("msg_id").Visualize(), 10),
|
|
22761
22769
|
sender_id: m.Reduce("sender_id").Visualize(),
|
|
22762
22770
|
sender_name: m.Reduce("sender_name").Visualize(),
|
|
22763
22771
|
text: m.Reduce("text").Visualize(),
|
|
22764
|
-
date: m.Reduce("date").Visualize()
|
|
22772
|
+
date: m.Reduce("date").Visualize(),
|
|
22773
|
+
status: m.Reduce("status").Visualize()
|
|
22765
22774
|
});
|
|
22766
22775
|
}
|
|
22767
22776
|
return out;
|
|
@@ -22769,12 +22778,32 @@ function renderInbox(v) {
|
|
|
22769
22778
|
function textResult(text, isError = false) {
|
|
22770
22779
|
return { content: [{ type: "text", text }], isError };
|
|
22771
22780
|
}
|
|
22781
|
+
var INVITE_BROTLI_V1 = 1;
|
|
22782
|
+
function packInvite(raw) {
|
|
22783
|
+
const compressed = brotliCompressSync(raw, {
|
|
22784
|
+
params: {
|
|
22785
|
+
[zlibConstants.BROTLI_PARAM_QUALITY]: 11,
|
|
22786
|
+
[zlibConstants.BROTLI_PARAM_SIZE_HINT]: raw.length
|
|
22787
|
+
}
|
|
22788
|
+
});
|
|
22789
|
+
return Buffer.concat([Buffer.from([INVITE_BROTLI_V1]), compressed]).toString("base64");
|
|
22790
|
+
}
|
|
22791
|
+
function unpackInvite(b64) {
|
|
22792
|
+
const outer = Buffer.from(b64.trim(), "base64");
|
|
22793
|
+
if (outer.length < 2) throw new Error("the invite blob is too short or not valid base64");
|
|
22794
|
+
const version2 = outer[0];
|
|
22795
|
+
if (version2 === INVITE_BROTLI_V1) return Buffer.from(brotliDecompressSync(outer.subarray(1)));
|
|
22796
|
+
throw new Error(`unsupported invite format (version ${version2}) \u2014 the sender may be on a different a2adapt version`);
|
|
22797
|
+
}
|
|
22798
|
+
function fmtMsg(m, withStatus = true) {
|
|
22799
|
+
const status = withStatus && m.status && m.status !== "unread" ? ` [${m.status}]` : "";
|
|
22800
|
+
return `#${m.msg_id} [${m.sender_name}]${status} ${m.text} (${m.date})`;
|
|
22801
|
+
}
|
|
22772
22802
|
function createMcpServer(getSessionId) {
|
|
22773
22803
|
const server = new McpServer(
|
|
22774
22804
|
{ name: "a2adapt", version: VERSION },
|
|
22775
22805
|
{ capabilities: { logging: {}, resources: {} } }
|
|
22776
22806
|
);
|
|
22777
|
-
activeMcpServers.add(server);
|
|
22778
22807
|
const boundOr = () => {
|
|
22779
22808
|
const b = resolveBound(getSessionId());
|
|
22780
22809
|
return "error" in b ? { err: textResult(b.error, true) } : { id: b.id };
|
|
@@ -22885,7 +22914,7 @@ ${lines.join("\n")}`);
|
|
|
22885
22914
|
if (err || !id) return { contents: [{ uri, mimeType: "text/plain", text: "No identity bound to this session." }] };
|
|
22886
22915
|
const inbox = renderInbox(readonlyTx(id, "::actor::list_incoming_messages"));
|
|
22887
22916
|
const text = inbox.length === 0 ? "Inbox is empty." : `Inbox (${inbox.length}):
|
|
22888
|
-
${inbox.map((m
|
|
22917
|
+
${inbox.map((m) => fmtMsg(m)).join("\n")}`;
|
|
22889
22918
|
return { contents: [{ uri, mimeType: "text/plain", text }] };
|
|
22890
22919
|
}
|
|
22891
22920
|
);
|
|
@@ -22898,7 +22927,7 @@ ${inbox.map((m, i) => `${i + 1}. [${m.sender_name}] ${m.text} (${m.date})`).joi
|
|
|
22898
22927
|
if (err) return err;
|
|
22899
22928
|
try {
|
|
22900
22929
|
const data = await mutatingTx(id, "::actor::generate_invite", { name });
|
|
22901
|
-
const blob = Buffer.from(data.Reduce("invite").GetBinary())
|
|
22930
|
+
const blob = packInvite(Buffer.from(data.Reduce("invite").GetBinary()));
|
|
22902
22931
|
return textResult(
|
|
22903
22932
|
`Invite for "${name}" created. Share this blob out-of-band (they paste it into add_contact):
|
|
22904
22933
|
|
|
@@ -22921,10 +22950,10 @@ ${blob}`
|
|
|
22921
22950
|
if (err) return err;
|
|
22922
22951
|
let buf;
|
|
22923
22952
|
try {
|
|
22924
|
-
buf =
|
|
22925
|
-
if (buf.length === 0) throw new Error("empty");
|
|
22926
|
-
} catch {
|
|
22927
|
-
return textResult(
|
|
22953
|
+
buf = unpackInvite(invite);
|
|
22954
|
+
if (buf.length === 0) throw new Error("the invite blob is empty");
|
|
22955
|
+
} catch (e) {
|
|
22956
|
+
return textResult(`add_contact failed: ${e instanceof Error ? e.message : "the invite blob is not valid."}`, true);
|
|
22928
22957
|
}
|
|
22929
22958
|
try {
|
|
22930
22959
|
const blobValue = id.pw.packet.NewBinaryFromBuffer(buf);
|
|
@@ -22976,7 +23005,7 @@ ${contacts.map((c) => `\u2022 ${c.name} \u2014 ${c.container_id}`).join("\n")}`)
|
|
|
22976
23005
|
);
|
|
22977
23006
|
server.tool(
|
|
22978
23007
|
"list_incoming_messages",
|
|
22979
|
-
"List
|
|
23008
|
+
"List ALL messages in the bound identity's inbox (decrypted), each with its id and status (unread/read). A read-only history view \u2014 it does not change any status. To consume new mail use get_messages instead.",
|
|
22980
23009
|
{},
|
|
22981
23010
|
async () => {
|
|
22982
23011
|
const { id, err } = boundOr();
|
|
@@ -22984,30 +23013,70 @@ ${contacts.map((c) => `\u2022 ${c.name} \u2014 ${c.container_id}`).join("\n")}`)
|
|
|
22984
23013
|
try {
|
|
22985
23014
|
const inbox = renderInbox(readonlyTx(id, "::actor::list_incoming_messages"));
|
|
22986
23015
|
if (inbox.length === 0) return textResult("Inbox is empty.");
|
|
22987
|
-
|
|
22988
|
-
|
|
23016
|
+
const unread = inbox.filter((m) => m.status === "unread").length;
|
|
23017
|
+
return textResult(
|
|
23018
|
+
`Inbox (${inbox.length}, ${unread} unread):
|
|
23019
|
+
${inbox.map((m) => fmtMsg(m)).join("\n")}`
|
|
23020
|
+
);
|
|
22989
23021
|
} catch (e) {
|
|
22990
23022
|
return textResult(`list_incoming_messages failed: ${String(e)}`, true);
|
|
22991
23023
|
}
|
|
22992
23024
|
}
|
|
22993
23025
|
);
|
|
22994
23026
|
server.tool(
|
|
22995
|
-
"
|
|
22996
|
-
|
|
23027
|
+
"get_messages",
|
|
23028
|
+
'Fetch the messages the bound identity has not seen yet (status "unread") and mark them "read". This is the ONLY call that returns message bodies. A message is delivered here exactly once, so reading and acting on it immediately will not double-process. Note each message id: pass them to mark_processed when done, or to defer_messages to leave a message for another session.',
|
|
22997
23029
|
{},
|
|
22998
23030
|
async () => {
|
|
22999
23031
|
const { id, err } = boundOr();
|
|
23000
23032
|
if (err) return err;
|
|
23001
23033
|
try {
|
|
23002
|
-
const
|
|
23003
|
-
const
|
|
23004
|
-
|
|
23005
|
-
writeCursor(id.dir, inbox.length);
|
|
23034
|
+
const data = await mutatingTx(id, "::actor::get_messages", {});
|
|
23035
|
+
const fresh = renderInbox(data.Reduce("messages"));
|
|
23036
|
+
refreshUnread(id);
|
|
23006
23037
|
if (fresh.length === 0) return textResult("No new messages.");
|
|
23007
|
-
return textResult(
|
|
23008
|
-
|
|
23038
|
+
return textResult(
|
|
23039
|
+
`${fresh.length} new message(s):
|
|
23040
|
+
${fresh.map((m) => fmtMsg(m, false)).join("\n")}
|
|
23041
|
+
|
|
23042
|
+
When handled: mark_processed({ msg_ids: [${fresh.map((m) => m.msg_id).join(", ")}] }). To leave any for another session: defer_messages({ msg_ids: [...] }).`
|
|
23043
|
+
);
|
|
23044
|
+
} catch (e) {
|
|
23045
|
+
return textResult(`get_messages failed: ${String(e)}`, true);
|
|
23046
|
+
}
|
|
23047
|
+
}
|
|
23048
|
+
);
|
|
23049
|
+
server.tool(
|
|
23050
|
+
"mark_processed",
|
|
23051
|
+
"Mark the given messages handled \u2014 they are removed from the inbox permanently. Idempotent: ids already gone are ignored. Optional for correctness (get_messages already prevents re-delivery); use it to keep the inbox tidy.",
|
|
23052
|
+
{ msg_ids: external_exports.array(external_exports.number().int()).min(1).describe("Message ids (from get_messages) to mark processed.") },
|
|
23053
|
+
async ({ msg_ids }) => {
|
|
23054
|
+
const { id, err } = boundOr();
|
|
23055
|
+
if (err) return err;
|
|
23056
|
+
try {
|
|
23057
|
+
const data = await mutatingTx(id, "::actor::mark_processed", { msg_ids });
|
|
23058
|
+
const n = data.Reduce("processed").Visualize();
|
|
23059
|
+
refreshUnread(id);
|
|
23060
|
+
return textResult(`Marked ${n} message(s) processed.`);
|
|
23061
|
+
} catch (e) {
|
|
23062
|
+
return textResult(`mark_processed failed: ${String(e)}`, true);
|
|
23063
|
+
}
|
|
23064
|
+
}
|
|
23065
|
+
);
|
|
23066
|
+
server.tool(
|
|
23067
|
+
"defer_messages",
|
|
23068
|
+
`Put already-read messages back into the queue (status "unread") so another session's get_messages will pick them up. Use when you read a message but want to leave it for someone else to process.`,
|
|
23069
|
+
{ msg_ids: external_exports.array(external_exports.number().int()).min(1).describe("Message ids (from get_messages) to defer back to unread.") },
|
|
23070
|
+
async ({ msg_ids }) => {
|
|
23071
|
+
const { id, err } = boundOr();
|
|
23072
|
+
if (err) return err;
|
|
23073
|
+
try {
|
|
23074
|
+
const data = await mutatingTx(id, "::actor::defer_messages", { msg_ids });
|
|
23075
|
+
const n = data.Reduce("deferred").Visualize();
|
|
23076
|
+
refreshUnread(id);
|
|
23077
|
+
return textResult(`Deferred ${n} message(s) back to unread.`);
|
|
23009
23078
|
} catch (e) {
|
|
23010
|
-
return textResult(`
|
|
23079
|
+
return textResult(`defer_messages failed: ${String(e)}`, true);
|
|
23011
23080
|
}
|
|
23012
23081
|
}
|
|
23013
23082
|
);
|
|
@@ -23030,6 +23099,7 @@ function readBody(req) {
|
|
|
23030
23099
|
async function main() {
|
|
23031
23100
|
if (TRANSPORT === "stdio") {
|
|
23032
23101
|
const server = createMcpServer(() => "stdio");
|
|
23102
|
+
serversBySession.set("stdio", server);
|
|
23033
23103
|
const transport = new StdioServerTransport();
|
|
23034
23104
|
await server.connect(transport);
|
|
23035
23105
|
log("MCP stdio transport connected, booting wrapper\u2026");
|
|
@@ -23065,6 +23135,7 @@ async function main() {
|
|
|
23065
23135
|
sessionIdGenerator: () => randomUUID(),
|
|
23066
23136
|
onsessioninitialized: (sid) => {
|
|
23067
23137
|
transports[sid] = transport;
|
|
23138
|
+
serversBySession.set(sid, server);
|
|
23068
23139
|
log(`session ${sid.slice(0, 8)}\u2026 initialized`);
|
|
23069
23140
|
}
|
|
23070
23141
|
});
|
|
@@ -23073,13 +23144,13 @@ async function main() {
|
|
|
23073
23144
|
const sid = transport.sessionId;
|
|
23074
23145
|
if (sid) {
|
|
23075
23146
|
delete transports[sid];
|
|
23147
|
+
serversBySession.delete(sid);
|
|
23076
23148
|
const name = sessionBinding.get(sid);
|
|
23077
23149
|
if (name && bindingOwner.get(name) === sid) bindingOwner.delete(name);
|
|
23078
23150
|
sessionBinding.delete(sid);
|
|
23079
23151
|
evictedSessions.delete(sid);
|
|
23080
23152
|
log(`session ${sid.slice(0, 8)}\u2026 closed`);
|
|
23081
23153
|
}
|
|
23082
|
-
activeMcpServers.delete(server);
|
|
23083
23154
|
};
|
|
23084
23155
|
await server.connect(transport);
|
|
23085
23156
|
await transport.handleRequest(req, res, body);
|
|
Binary file
|
package/dist/mufl_code/actor.mu
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// User transactions (each backs one MCP tool):
|
|
9
9
|
// set_my_name — set the display name peers see for me
|
|
10
|
-
// generate_invite — make a personal invite blob for a named peer
|
|
10
|
+
// generate_invite — make a slim personal invite blob for a named peer
|
|
11
11
|
// add_contact — join via an invite blob, reply to the inviter
|
|
12
12
|
// send_message — send an e2e-encrypted message to a contact
|
|
13
13
|
// list_contacts — (readonly) my contacts
|
|
14
|
-
// list_incoming_messages — (readonly) my inbox
|
|
14
|
+
// list_incoming_messages — (readonly) my inbox, with per-message id + status
|
|
15
|
+
// get_messages — return unread messages + mark them read (sole body egress)
|
|
16
|
+
// mark_processed — delete handled messages from the inbox
|
|
17
|
+
// defer_messages — flip read messages back to unread for another session
|
|
15
18
|
//
|
|
16
19
|
// External transactions (inbound, not exposed as tools):
|
|
17
20
|
// accept_contact — inviter learns the joiner's identity + name
|
|
@@ -39,8 +42,27 @@ application actor loads libraries
|
|
|
39
42
|
hidden
|
|
40
43
|
{
|
|
41
44
|
metadef contact_t: ($name -> str, $container_id -> global_id).
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// Slim invite. Short keys (no field-name bloat), no outer wrapper, and
|
|
46
|
+
// only the cryptographically load-bearing parts travel: the inviter's
|
|
47
|
+
// signed identity (public keys) + its self-signatures. inviter_id is NOT
|
|
48
|
+
// carried separately — it is the identity's own container_id. version is
|
|
49
|
+
// a constant reconstructed on the receiver. (See generate_invite /
|
|
50
|
+
// add_contact: the embedded identity is kept byte-for-byte so its
|
|
51
|
+
// _value_id — what the signatures are over — stays valid.)
|
|
52
|
+
// $d invite_id $n inviter_name $c container_id
|
|
53
|
+
// $k public keys $a authorizations
|
|
54
|
+
// default_keys is NOT carried — the receiver rebuilds it from the keys
|
|
55
|
+
// (each key knows its own function + id), so the reconstructed identity is
|
|
56
|
+
// byte-identical to the signed one and the self-signatures still verify.
|
|
57
|
+
metadef invite_t: ($d -> global_id, $n -> str, $c -> global_id, $k -> key_utils::t_publickey(,), $a -> crypto_signature(,)).
|
|
58
|
+
// A received message carries a stable per-packet id and a lifecycle
|
|
59
|
+
// status: "unread" (just arrived) -> "read" (handed to the agent via
|
|
60
|
+
// get_messages). mark_processed deletes it; defer_messages flips it back
|
|
61
|
+
// to "unread" so another session can pick it up.
|
|
62
|
+
metadef message_t: ($msg_id -> int, $sender_id -> global_id, $sender_name -> str, $text -> str, $date -> time, $status -> str).
|
|
63
|
+
// The pre-lifecycle inbox shape (no per-message id/status). import_state
|
|
64
|
+
// migrates blobs in this shape forward — see below.
|
|
65
|
+
metadef legacy_message_t: ($sender_id -> global_id, $sender_name -> str, $text -> str, $date -> time).
|
|
44
66
|
|
|
45
67
|
// Wire the deserialization primitive into the libraries that need it.
|
|
46
68
|
_read_or_abort = grab( _read_or_abort ).
|
|
@@ -54,8 +76,14 @@ application actor loads libraries
|
|
|
54
76
|
contacts is (global_id ->> contact_t) = (,).
|
|
55
77
|
// Invites I generated, keyed by invite id -> the name I assigned the peer.
|
|
56
78
|
pending_invites is (global_id ->> str) = (,).
|
|
57
|
-
// Received messages
|
|
79
|
+
// Received messages. Each carries its own lifecycle status (see
|
|
80
|
+
// message_t / get_messages / mark_processed / defer_messages), so the
|
|
81
|
+
// packet is the single authority on what has been read or processed —
|
|
82
|
+
// no host-side cursor, safe across concurrent sessions.
|
|
58
83
|
inbox is message_t[] = [].
|
|
84
|
+
// Monotonic source of per-message ids (the stable handle the agent uses
|
|
85
|
+
// to mark a message processed or defer it).
|
|
86
|
+
next_msg_seq is int = 1.
|
|
59
87
|
// Peer address documents, captured when a contact is established. These are
|
|
60
88
|
// self-signed, code-independent, and seed-stable, so on a code upgrade the
|
|
61
89
|
// host re-creates this packet from the same seed and import_state replays
|
|
@@ -105,13 +133,20 @@ application actor loads libraries
|
|
|
105
133
|
// Remember the name I'm assigning to whoever redeems this invite.
|
|
106
134
|
pending_invites invite_id -> name.
|
|
107
135
|
|
|
136
|
+
// Carry only my signed identity (public keys) + its self-signatures. The
|
|
137
|
+
// joiner rebuilds my address document from these (version is constant, my
|
|
138
|
+
// container_id is the identity's own field) and stores it so it can
|
|
139
|
+
// re-register me after a code upgrade (see peer_ads / import_state). The
|
|
140
|
+
// identity sub-record is passed through untouched so its _value_id — what
|
|
141
|
+
// the authorizations sign over — survives the round trip unchanged.
|
|
142
|
+
my_ad = address_document::get_my_address_document().
|
|
143
|
+
my_identity = my_ad $identity.
|
|
108
144
|
invite is invite_t = (
|
|
109
|
-
$
|
|
110
|
-
$
|
|
111
|
-
$
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
$inviter_ad -> address_document::get_my_address_document()
|
|
145
|
+
$d -> invite_id,
|
|
146
|
+
$n -> my_name,
|
|
147
|
+
$c -> my_identity $container_id,
|
|
148
|
+
$k -> my_identity $key_list,
|
|
149
|
+
$a -> my_ad $authorizations
|
|
115
150
|
).
|
|
116
151
|
|
|
117
152
|
return transaction::success [
|
|
@@ -129,17 +164,41 @@ application actor loads libraries
|
|
|
129
164
|
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
130
165
|
|
|
131
166
|
invite = (_read_or_abort invite_blob) safe invite_t.
|
|
132
|
-
inviter_id = invite $
|
|
167
|
+
inviter_id = invite $c.
|
|
133
168
|
abort "This invite is your own — you cannot add yourself." when inviter_id == _get_container_id().
|
|
134
169
|
|
|
170
|
+
// Rebuild default_keys from the carried public keys: each key reports its
|
|
171
|
+
// own function and id, so this reproduces the inviter's default-key map
|
|
172
|
+
// exactly. With it we reconstruct the full identity (and hence its
|
|
173
|
+
// _value_id, which the self-signatures sign over) byte-for-byte, then the
|
|
174
|
+
// full address document. import_state later replays this reconstructed
|
|
175
|
+
// document through process_address_document to re-register the inviter's
|
|
176
|
+
// keys after a code upgrade — so it must validate, and it does.
|
|
177
|
+
inviter_keys = invite $k.
|
|
178
|
+
inviter_default_keys is (key_utils::t_function ->> key_utils::t_key_id) = (,).
|
|
179
|
+
sc inviter_keys -- (key -> )
|
|
180
|
+
{
|
|
181
|
+
inviter_default_keys (key_utils::key_get_function key) -> (_crypto_get_key_id key).
|
|
182
|
+
}
|
|
183
|
+
inviter_identity is key_storage::t_container_identity = (
|
|
184
|
+
$key_list -> inviter_keys,
|
|
185
|
+
$default_keys -> inviter_default_keys,
|
|
186
|
+
$container_id -> inviter_id
|
|
187
|
+
).
|
|
188
|
+
inviter_ad is address_document_types::t_address_document = (
|
|
189
|
+
$version -> 1,
|
|
190
|
+
$identity -> inviter_identity,
|
|
191
|
+
$authorizations -> invite $a
|
|
192
|
+
).
|
|
193
|
+
|
|
135
194
|
// Register the inviter under my chosen name, or the name they embedded.
|
|
136
|
-
contact_name = (custom_name == NIL ?? (invite $
|
|
195
|
+
contact_name = (custom_name == NIL ?? (invite $n) ; custom_name?).
|
|
137
196
|
contacts inviter_id -> ($name -> contact_name, $container_id -> inviter_id).
|
|
138
197
|
// Remember the inviter's address document so I can re-register them after
|
|
139
198
|
// an upgrade (their keys are seed-stable, so this stays valid).
|
|
140
|
-
peer_ads inviter_id ->
|
|
199
|
+
peer_ads inviter_id -> inviter_ad.
|
|
141
200
|
|
|
142
|
-
invite_id = invite $
|
|
201
|
+
invite_id = invite $d.
|
|
143
202
|
my_self_name = my_name.
|
|
144
203
|
my_ad = address_document::get_my_address_document().
|
|
145
204
|
|
|
@@ -187,6 +246,117 @@ application actor loads libraries
|
|
|
187
246
|
return inbox.
|
|
188
247
|
}
|
|
189
248
|
|
|
249
|
+
// Hand the agent every message it has not seen yet (status "unread") and
|
|
250
|
+
// flip those to "read". This is the ONLY place message bodies leave the
|
|
251
|
+
// packet. Delivery is the dedup point: a message is returned by get_messages
|
|
252
|
+
// exactly once, so an agent that reads and acts immediately never double-
|
|
253
|
+
// processes — no explicit acknowledgement is required for safety. To leave a
|
|
254
|
+
// message for another session instead, call defer_messages (status -> unread
|
|
255
|
+
// again); to discard it for good, call mark_processed (removed from inbox).
|
|
256
|
+
trn get_messages _
|
|
257
|
+
{
|
|
258
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
259
|
+
|
|
260
|
+
fresh is message_t[] = [].
|
|
261
|
+
new_inbox is message_t[] = [].
|
|
262
|
+
sc inbox -- ( -> m)
|
|
263
|
+
{
|
|
264
|
+
if (m $status) == "unread"
|
|
265
|
+
{
|
|
266
|
+
read_m is message_t = (
|
|
267
|
+
$msg_id -> m $msg_id,
|
|
268
|
+
$sender_id -> m $sender_id,
|
|
269
|
+
$sender_name -> m $sender_name,
|
|
270
|
+
$text -> m $text,
|
|
271
|
+
$date -> m $date,
|
|
272
|
+
$status -> "read"
|
|
273
|
+
).
|
|
274
|
+
fresh (_count fresh|) -> m.
|
|
275
|
+
new_inbox (_count new_inbox|) -> read_m.
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
{
|
|
279
|
+
new_inbox (_count new_inbox|) -> m.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
inbox -> new_inbox.
|
|
283
|
+
|
|
284
|
+
return transaction::success [
|
|
285
|
+
_return_data ($messages -> fresh),
|
|
286
|
+
_save_state NIL
|
|
287
|
+
].
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Remove the given messages from the inbox permanently — the agent is done
|
|
291
|
+
// with them. Idempotent: ids that are already gone are silently skipped.
|
|
292
|
+
trn mark_processed _:($msg_ids -> ids: int[])
|
|
293
|
+
{
|
|
294
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
295
|
+
|
|
296
|
+
wanted is (int ->> bool) = (,).
|
|
297
|
+
sc ids -- ( -> id) { wanted id -> TRUE. }
|
|
298
|
+
|
|
299
|
+
new_inbox is message_t[] = [].
|
|
300
|
+
removed is int = 0.
|
|
301
|
+
sc inbox -- ( -> m)
|
|
302
|
+
{
|
|
303
|
+
if wanted (m $msg_id)
|
|
304
|
+
{
|
|
305
|
+
removed -> removed + 1.
|
|
306
|
+
}
|
|
307
|
+
else
|
|
308
|
+
{
|
|
309
|
+
new_inbox (_count new_inbox|) -> m.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
inbox -> new_inbox.
|
|
313
|
+
|
|
314
|
+
return transaction::success [
|
|
315
|
+
_return_data ($processed -> removed),
|
|
316
|
+
_save_state NIL
|
|
317
|
+
].
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Put already-read messages back into the queue (status -> "unread") so a
|
|
321
|
+
// different session will pick them up on its next get_messages. The explicit,
|
|
322
|
+
// opt-in counterpart to the safe default: forgetting to defer just means the
|
|
323
|
+
// message stays handled by you; only a deliberate defer re-exposes it.
|
|
324
|
+
trn defer_messages _:($msg_ids -> ids: int[])
|
|
325
|
+
{
|
|
326
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
327
|
+
|
|
328
|
+
wanted is (int ->> bool) = (,).
|
|
329
|
+
sc ids -- ( -> id) { wanted id -> TRUE. }
|
|
330
|
+
|
|
331
|
+
new_inbox is message_t[] = [].
|
|
332
|
+
deferred is int = 0.
|
|
333
|
+
sc inbox -- ( -> m)
|
|
334
|
+
{
|
|
335
|
+
if (wanted (m $msg_id)) && ((m $status) == "read")
|
|
336
|
+
{
|
|
337
|
+
new_inbox (_count new_inbox|) -> (
|
|
338
|
+
$msg_id -> m $msg_id,
|
|
339
|
+
$sender_id -> m $sender_id,
|
|
340
|
+
$sender_name -> m $sender_name,
|
|
341
|
+
$text -> m $text,
|
|
342
|
+
$date -> m $date,
|
|
343
|
+
$status -> "unread"
|
|
344
|
+
).
|
|
345
|
+
deferred -> deferred + 1.
|
|
346
|
+
}
|
|
347
|
+
else
|
|
348
|
+
{
|
|
349
|
+
new_inbox (_count new_inbox|) -> m.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
inbox -> new_inbox.
|
|
353
|
+
|
|
354
|
+
return transaction::success [
|
|
355
|
+
_return_data ($deferred -> deferred),
|
|
356
|
+
_save_state NIL
|
|
357
|
+
].
|
|
358
|
+
}
|
|
359
|
+
|
|
190
360
|
// ---- upgrade: state export / import -------------------------------------
|
|
191
361
|
// The host persists state by calling export_state (readonly) and serializing
|
|
192
362
|
// the returned value to a code-independent blob. On a code upgrade it recreates
|
|
@@ -203,6 +373,7 @@ application actor loads libraries
|
|
|
203
373
|
$contacts -> contacts,
|
|
204
374
|
$pending_invites -> pending_invites,
|
|
205
375
|
$inbox -> inbox,
|
|
376
|
+
$next_msg_seq -> next_msg_seq,
|
|
206
377
|
$peer_ads -> peer_ads
|
|
207
378
|
).
|
|
208
379
|
}
|
|
@@ -211,19 +382,44 @@ application actor loads libraries
|
|
|
211
382
|
{
|
|
212
383
|
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
213
384
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
385
|
+
// The fields that did not change across the schema bump are validated the
|
|
386
|
+
// same way for any version of the blob.
|
|
387
|
+
my_name -> (data $my_name) safe str.
|
|
388
|
+
contacts -> (data $contacts) safe (global_id ->> contact_t).
|
|
389
|
+
pending_invites -> (data $pending_invites) safe (global_id ->> str).
|
|
390
|
+
peer_ads -> (data $peer_ads) safe (global_id ->> address_document_types::t_address_document).
|
|
391
|
+
|
|
392
|
+
// The inbox + next_msg_seq are the only parts the message-lifecycle change
|
|
393
|
+
// touched. A pre-lifecycle blob has no $next_msg_seq and inbox entries with
|
|
394
|
+
// no id/status — MIGRATE it forward (the whole point of code-independent
|
|
395
|
+
// state is that an old export upgrades, never resets): assign each legacy
|
|
396
|
+
// message a sequential id and status "unread", and seed next_msg_seq past
|
|
397
|
+
// them. A current blob is taken as-is.
|
|
398
|
+
if (data $next_msg_seq) == NIL
|
|
399
|
+
{
|
|
400
|
+
legacy_inbox = (data $inbox) safe (legacy_message_t[]).
|
|
401
|
+
migrated is message_t[] = [].
|
|
402
|
+
seq is int = 1.
|
|
403
|
+
sc legacy_inbox -- ( -> m)
|
|
404
|
+
{
|
|
405
|
+
migrated (_count migrated|) -> (
|
|
406
|
+
$msg_id -> seq,
|
|
407
|
+
$sender_id -> m $sender_id,
|
|
408
|
+
$sender_name -> m $sender_name,
|
|
409
|
+
$text -> m $text,
|
|
410
|
+
$date -> m $date,
|
|
411
|
+
$status -> "unread"
|
|
412
|
+
).
|
|
413
|
+
seq -> seq + 1.
|
|
414
|
+
}
|
|
415
|
+
inbox -> migrated.
|
|
416
|
+
next_msg_seq -> seq.
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
{
|
|
420
|
+
inbox -> (data $inbox) safe (message_t[]).
|
|
421
|
+
next_msg_seq -> (data $next_msg_seq) safe int.
|
|
422
|
+
}
|
|
227
423
|
|
|
228
424
|
// Re-register every peer's keys so encrypted channels keep working after
|
|
229
425
|
// the upgrade — no handshake needed (my own keys are unchanged, and the
|
|
@@ -276,15 +472,23 @@ application actor loads libraries
|
|
|
276
472
|
sender_name = sender? $name.
|
|
277
473
|
msg_date = current_transaction_info::get_transaction_time().
|
|
278
474
|
|
|
475
|
+
mid = next_msg_seq.
|
|
476
|
+
next_msg_seq -> next_msg_seq + 1.
|
|
477
|
+
|
|
279
478
|
inbox (_count inbox|) -> (
|
|
479
|
+
$msg_id -> mid,
|
|
280
480
|
$sender_id -> sender_id,
|
|
281
481
|
$sender_name -> sender_name,
|
|
282
482
|
$text -> text,
|
|
283
|
-
$date -> msg_date
|
|
483
|
+
$date -> msg_date,
|
|
484
|
+
$status -> "unread"
|
|
284
485
|
).
|
|
285
486
|
|
|
487
|
+
// The notification deliberately carries NO message body — only that a
|
|
488
|
+
// message arrived, from whom, and its id. The body stays in the packet
|
|
489
|
+
// and only leaves through get_messages.
|
|
286
490
|
return transaction::success [
|
|
287
|
-
_notify_agent ($event -> $message_received, $sender_name -> sender_name, $
|
|
491
|
+
_notify_agent ($event -> $message_received, $sender_name -> sender_name, $msg_id -> mid, $date -> msg_date),
|
|
288
492
|
_save_state NIL
|
|
289
493
|
].
|
|
290
494
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adapt-toolkit/a2adapt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server daemon for a2adapt — one native ADAPT wrapper hosting N self-sovereign identities, exposing secure agent-to-agent messaging tools over HTTP (Streamable HTTP). Run `a2adapt-mcp start`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|