agent-inbox 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +113 -0
- package/README.md +195 -1
- package/dist/federation/address.d.ts +24 -0
- package/dist/federation/address.d.ts.map +1 -0
- package/dist/federation/address.js +54 -0
- package/dist/federation/address.js.map +1 -0
- package/dist/federation/connection-manager.d.ts +118 -0
- package/dist/federation/connection-manager.d.ts.map +1 -0
- package/dist/federation/connection-manager.js +369 -0
- package/dist/federation/connection-manager.js.map +1 -0
- package/dist/federation/delivery-queue.d.ts +66 -0
- package/dist/federation/delivery-queue.d.ts.map +1 -0
- package/dist/federation/delivery-queue.js +199 -0
- package/dist/federation/delivery-queue.js.map +1 -0
- package/dist/federation/index.d.ts +7 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +6 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/federation/routing-engine.d.ts +74 -0
- package/dist/federation/routing-engine.d.ts.map +1 -0
- package/dist/federation/routing-engine.js +158 -0
- package/dist/federation/routing-engine.js.map +1 -0
- package/dist/federation/trust.d.ts +39 -0
- package/dist/federation/trust.d.ts.map +1 -0
- package/dist/federation/trust.js +64 -0
- package/dist/federation/trust.js.map +1 -0
- package/dist/index.d.ts +60 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +217 -18
- package/dist/index.js.map +1 -1
- package/dist/ipc/ipc-server.d.ts +21 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -0
- package/dist/ipc/ipc-server.js +173 -0
- package/dist/ipc/ipc-server.js.map +1 -0
- package/dist/jsonrpc/mail-server.d.ts +45 -0
- package/dist/jsonrpc/mail-server.d.ts.map +1 -0
- package/dist/jsonrpc/mail-server.js +284 -0
- package/dist/jsonrpc/mail-server.js.map +1 -0
- package/dist/map/map-client.d.ts +91 -0
- package/dist/map/map-client.d.ts.map +1 -0
- package/dist/map/map-client.js +202 -0
- package/dist/map/map-client.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +23 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +226 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/push/notifier.d.ts +49 -0
- package/dist/push/notifier.d.ts.map +1 -0
- package/dist/push/notifier.js +150 -0
- package/dist/push/notifier.js.map +1 -0
- package/dist/registry/warm-registry.d.ts +63 -0
- package/dist/registry/warm-registry.d.ts.map +1 -0
- package/dist/registry/warm-registry.js +173 -0
- package/dist/registry/warm-registry.js.map +1 -0
- package/dist/router/message-router.d.ts +44 -0
- package/dist/router/message-router.d.ts.map +1 -0
- package/dist/router/message-router.js +137 -0
- package/dist/router/message-router.js.map +1 -0
- package/dist/storage/interface.d.ts +31 -0
- package/dist/storage/interface.d.ts.map +1 -0
- package/dist/storage/interface.js +2 -0
- package/dist/storage/interface.js.map +1 -0
- package/dist/storage/memory.d.ts +28 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +118 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +445 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/traceability/traceability.d.ts +29 -0
- package/dist/traceability/traceability.d.ts.map +1 -0
- package/dist/traceability/traceability.js +150 -0
- package/dist/traceability/traceability.js.map +1 -0
- package/dist/types.d.ts +261 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/docs/DESIGN.md +1156 -0
- package/docs/PLAN.md +545 -0
- package/hooks/inbox-hook.mjs +119 -0
- package/hooks/register-hook.mjs +69 -0
- package/package.json +33 -25
- package/rules/agent-inbox.md +78 -0
- package/src/federation/address.ts +61 -0
- package/src/federation/connection-manager.ts +458 -0
- package/src/federation/delivery-queue.ts +222 -0
- package/src/federation/index.ts +6 -0
- package/src/federation/routing-engine.ts +188 -0
- package/src/federation/trust.ts +71 -0
- package/src/index.ts +299 -0
- package/src/ipc/ipc-server.ts +207 -0
- package/src/jsonrpc/mail-server.ts +356 -0
- package/src/map/map-client.ts +260 -0
- package/src/mcp/mcp-server.ts +272 -0
- package/src/push/notifier.ts +192 -0
- package/src/registry/warm-registry.ts +210 -0
- package/src/router/message-router.ts +175 -0
- package/src/storage/interface.ts +48 -0
- package/src/storage/memory.ts +145 -0
- package/src/storage/sqlite.ts +645 -0
- package/src/traceability/traceability.ts +183 -0
- package/src/types.ts +297 -0
- package/test/federation/address.test.ts +101 -0
- package/test/federation/connection-manager.test.ts +546 -0
- package/test/federation/delivery-queue.test.ts +159 -0
- package/test/federation/integration.test.ts +823 -0
- package/test/federation/routing-engine.test.ts +117 -0
- package/test/federation/sdk-integration.test.ts +748 -0
- package/test/federation/trust.test.ts +89 -0
- package/test/ipc-jsonrpc.test.ts +113 -0
- package/test/ipc-server.test.ts +197 -0
- package/test/mail-server.test.ts +208 -0
- package/test/map-client.test.ts +408 -0
- package/test/message-router.test.ts +184 -0
- package/test/push-notifier.test.ts +139 -0
- package/test/registry/warm-registry.test.ts +171 -0
- package/test/sqlite-storage.test.ts +243 -0
- package/test/storage.test.ts +196 -0
- package/test/traceability.test.ts +123 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +8 -0
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { Storage } from "../storage/interface.js";
|
|
5
|
+
import type { MessageRouter } from "../router/message-router.js";
|
|
6
|
+
import { normalizeContent } from "../router/message-router.js";
|
|
7
|
+
import { ulid } from "ulid";
|
|
8
|
+
import type { Message, Recipient, InboxConfig } from "../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrapper around @multi-agent-protocol/sdk for Agent Inbox's MAP connection.
|
|
12
|
+
*
|
|
13
|
+
* The MAP SDK is an optional peer dependency. This client gracefully degrades
|
|
14
|
+
* to a no-op when the SDK is not installed.
|
|
15
|
+
*/
|
|
16
|
+
export class MapClient {
|
|
17
|
+
private conn: MapConnection | null = null;
|
|
18
|
+
private agentConnectionClass: MapAgentConnectionClass | null = null;
|
|
19
|
+
private externalConn = false;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private storage: Storage,
|
|
23
|
+
private router: MessageRouter,
|
|
24
|
+
private events: EventEmitter,
|
|
25
|
+
private inboxJsonlPath?: string
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
async connect(config: NonNullable<InboxConfig["map"]>): Promise<boolean> {
|
|
29
|
+
if (!config.enabled || !config.server) return false;
|
|
30
|
+
|
|
31
|
+
let AgentConnection: MapAgentConnectionClass;
|
|
32
|
+
try {
|
|
33
|
+
// Dynamic import — optional peer dependency
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const sdk = await (Function('return import("@multi-agent-protocol/sdk")')() as Promise<{ AgentConnection: MapAgentConnectionClass }>);
|
|
36
|
+
AgentConnection = sdk.AgentConnection;
|
|
37
|
+
this.agentConnectionClass = AgentConnection;
|
|
38
|
+
} catch {
|
|
39
|
+
// MAP SDK not installed — standalone mode
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
this.conn = await AgentConnection.connect(config.server, {
|
|
45
|
+
name: "agent-inbox",
|
|
46
|
+
role: "inbox",
|
|
47
|
+
scopes: [config.scope ?? "default"],
|
|
48
|
+
capabilities: { trajectory: { canReport: false } },
|
|
49
|
+
metadata: {
|
|
50
|
+
systemId: config.systemId ?? "agent-inbox",
|
|
51
|
+
type: "agent-inbox",
|
|
52
|
+
},
|
|
53
|
+
reconnection: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
maxRetries: 5,
|
|
56
|
+
baseDelayMs: 1000,
|
|
57
|
+
maxDelayMs: 30000,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.conn.onMessage((msg: IncomingMapMessage) => {
|
|
62
|
+
this.handleIncoming(msg);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Use an externally-created MAP connection instead of creating one.
|
|
73
|
+
* The caller owns the connection lifecycle — disconnect() will detach
|
|
74
|
+
* but not close it. Incoming messages are still routed into storage.
|
|
75
|
+
*/
|
|
76
|
+
useConnection(conn: MapConnection): void {
|
|
77
|
+
this.conn = conn;
|
|
78
|
+
this.externalConn = true;
|
|
79
|
+
conn.onMessage((msg: IncomingMapMessage) => {
|
|
80
|
+
this.handleIncoming(msg);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async disconnect(): Promise<void> {
|
|
85
|
+
if (this.conn) {
|
|
86
|
+
if (!this.externalConn) {
|
|
87
|
+
await this.conn.disconnect();
|
|
88
|
+
}
|
|
89
|
+
this.conn = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async sendViaMap(
|
|
94
|
+
target: { agentId?: string; scope?: string },
|
|
95
|
+
payload: unknown
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
if (!this.conn) return false;
|
|
98
|
+
try {
|
|
99
|
+
await this.conn.send(target, payload);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get connected(): boolean {
|
|
107
|
+
return this.conn !== null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the underlying MAP connection, if connected.
|
|
112
|
+
* Returns the full AgentConnection instance from the SDK, exposing
|
|
113
|
+
* lifecycle methods (spawn, updateState, callExtension) beyond
|
|
114
|
+
* what Agent Inbox uses internally for messaging.
|
|
115
|
+
*/
|
|
116
|
+
getConnection(): MapConnection | null {
|
|
117
|
+
return this.conn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the system name from the MAP connection, if available.
|
|
122
|
+
* This is used for Tier 2 system ID resolution (MAP systemInfo).
|
|
123
|
+
*/
|
|
124
|
+
getSystemName(): string | undefined {
|
|
125
|
+
if (!this.conn) return undefined;
|
|
126
|
+
return this.conn.systemName;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the AgentConnection class from the MAP SDK, if available.
|
|
131
|
+
* Used to inject the SDK into ConnectionManager for federation peer connections.
|
|
132
|
+
*/
|
|
133
|
+
getAgentConnectionClass(): MapAgentConnectionClass | null {
|
|
134
|
+
return this.agentConnectionClass;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Startup replay: query MAP server for messages received while offline.
|
|
139
|
+
* Compares MAP server's message history against local storage and ingests
|
|
140
|
+
* any missing messages. Falls back to trusting local storage if the query fails.
|
|
141
|
+
*/
|
|
142
|
+
async replayMissed(): Promise<number> {
|
|
143
|
+
if (!this.conn) return 0;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Query MAP server for recent messages via extension
|
|
147
|
+
const result = await this.conn.callExtension?.(
|
|
148
|
+
"mail/recent",
|
|
149
|
+
{ limit: 200 }
|
|
150
|
+
) as { messages?: unknown[] } | undefined;
|
|
151
|
+
|
|
152
|
+
if (!result || !Array.isArray(result.messages)) return 0;
|
|
153
|
+
|
|
154
|
+
let ingested = 0;
|
|
155
|
+
for (const msg of result.messages as IncomingMapMessage[]) {
|
|
156
|
+
// Check if we already have this message (by timestamp + sender dedup)
|
|
157
|
+
const existing = this.findExistingMessage(msg);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
this.handleIncoming(msg);
|
|
160
|
+
ingested++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return ingested;
|
|
164
|
+
} catch {
|
|
165
|
+
// MAP server doesn't support mail/recent or query failed
|
|
166
|
+
// Fall back to trusting local storage
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private findExistingMessage(msg: IncomingMapMessage): boolean {
|
|
172
|
+
// Simple dedup: check if a message with the same sender and timestamp exists
|
|
173
|
+
if (!msg.timestamp) return false;
|
|
174
|
+
const candidates = this.storage.searchMessages(msg.from);
|
|
175
|
+
return candidates.some(
|
|
176
|
+
(m) => m.sender_id === msg.from && m.created_at === msg.timestamp
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private handleIncoming(msg: IncomingMapMessage): void {
|
|
181
|
+
const now = new Date().toISOString();
|
|
182
|
+
const content = normalizeContent(msg.payload);
|
|
183
|
+
|
|
184
|
+
// Store as a message
|
|
185
|
+
const message: Message = {
|
|
186
|
+
id: ulid(),
|
|
187
|
+
scope: "default",
|
|
188
|
+
sender_id: msg.from,
|
|
189
|
+
recipients: [] as Recipient[],
|
|
190
|
+
content,
|
|
191
|
+
importance: msg.meta?.priority === "high" ? "high" : "normal",
|
|
192
|
+
metadata: msg.meta ?? {},
|
|
193
|
+
created_at: msg.timestamp ?? now,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
this.storage.putMessage(message);
|
|
197
|
+
this.events.emit("message.created", message);
|
|
198
|
+
|
|
199
|
+
// Append to inbox.jsonl if configured
|
|
200
|
+
if (this.inboxJsonlPath) {
|
|
201
|
+
this.appendToInboxJsonl(msg);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private appendToInboxJsonl(msg: IncomingMapMessage): void {
|
|
206
|
+
if (!this.inboxJsonlPath) return;
|
|
207
|
+
try {
|
|
208
|
+
const dir = path.dirname(this.inboxJsonlPath);
|
|
209
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
210
|
+
const line = JSON.stringify({
|
|
211
|
+
from: msg.from,
|
|
212
|
+
timestamp: msg.timestamp ?? new Date().toISOString(),
|
|
213
|
+
payload: msg.payload,
|
|
214
|
+
meta: msg.meta,
|
|
215
|
+
});
|
|
216
|
+
fs.appendFileSync(this.inboxJsonlPath, line + "\n");
|
|
217
|
+
} catch {
|
|
218
|
+
// Best-effort write
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- MAP SDK type stubs (for when the SDK isn't installed) ---
|
|
224
|
+
|
|
225
|
+
export interface IncomingMapMessage {
|
|
226
|
+
from: string;
|
|
227
|
+
payload: unknown;
|
|
228
|
+
timestamp?: string;
|
|
229
|
+
meta?: { priority?: string; [key: string]: unknown };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface MapConnection {
|
|
233
|
+
send(
|
|
234
|
+
to: { agentId?: string; scope?: string },
|
|
235
|
+
payload: unknown,
|
|
236
|
+
meta?: Record<string, unknown>
|
|
237
|
+
): Promise<void>;
|
|
238
|
+
onMessage(handler: (msg: IncomingMapMessage) => void): void;
|
|
239
|
+
disconnect(): Promise<void>;
|
|
240
|
+
// Agent lifecycle (available on full SDK AgentConnection)
|
|
241
|
+
spawn?(opts: {
|
|
242
|
+
agentId: string;
|
|
243
|
+
name?: string;
|
|
244
|
+
role?: string;
|
|
245
|
+
scopes?: string[];
|
|
246
|
+
metadata?: Record<string, unknown>;
|
|
247
|
+
}): Promise<unknown>;
|
|
248
|
+
updateState?(state: string): Promise<void>;
|
|
249
|
+
updateMetadata?(metadata: Record<string, unknown>): Promise<void>;
|
|
250
|
+
callExtension?(
|
|
251
|
+
name: string,
|
|
252
|
+
params: Record<string, unknown>
|
|
253
|
+
): Promise<unknown>;
|
|
254
|
+
// System info (populated by some MAP servers)
|
|
255
|
+
systemName?: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface MapAgentConnectionClass {
|
|
259
|
+
connect(server: string, opts: unknown): Promise<MapConnection>;
|
|
260
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { Storage } from "../storage/interface.js";
|
|
5
|
+
import type { MessageRouter } from "../router/message-router.js";
|
|
6
|
+
import type { Agent } from "../types.js";
|
|
7
|
+
import type { WarmRegistry } from "../registry/warm-registry.js";
|
|
8
|
+
|
|
9
|
+
export class InboxMcpServer {
|
|
10
|
+
private mcp: McpServer;
|
|
11
|
+
private registry: WarmRegistry | null = null;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private router: MessageRouter,
|
|
15
|
+
private storage: Storage,
|
|
16
|
+
private defaultScope: string = "default"
|
|
17
|
+
) {
|
|
18
|
+
this.mcp = new McpServer({
|
|
19
|
+
name: "agent-inbox",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.registerTools();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attach a warm registry for federation-aware agent registration.
|
|
28
|
+
*/
|
|
29
|
+
setRegistry(registry: WarmRegistry): void {
|
|
30
|
+
this.registry = registry;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private registerTools(): void {
|
|
34
|
+
this.mcp.tool(
|
|
35
|
+
"send_message",
|
|
36
|
+
"Send a message to one or more agents. Supports replies (inReplyTo), threading (threadTag), and federated addressing (agent@system).",
|
|
37
|
+
{
|
|
38
|
+
to: z
|
|
39
|
+
.union([z.string(), z.array(z.string())])
|
|
40
|
+
.describe(
|
|
41
|
+
"Recipient agent ID(s). Use 'agent@system' for federated addressing."
|
|
42
|
+
),
|
|
43
|
+
body: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Plain text message body (shorthand for content)"),
|
|
47
|
+
content: z
|
|
48
|
+
.object({
|
|
49
|
+
type: z.string(),
|
|
50
|
+
})
|
|
51
|
+
.passthrough()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Structured message content"),
|
|
54
|
+
from: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Sender agent ID (defaults to caller)"),
|
|
58
|
+
threadTag: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Thread tag for grouping related messages"),
|
|
62
|
+
inReplyTo: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Message ID this is a reply to"),
|
|
66
|
+
importance: z
|
|
67
|
+
.enum(["low", "normal", "high", "urgent"])
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Message importance level"),
|
|
70
|
+
subject: z.string().optional().describe("Message subject"),
|
|
71
|
+
},
|
|
72
|
+
async ({ to, body, content, from, threadTag, inReplyTo, importance, subject }) => {
|
|
73
|
+
const payload = content ?? body ?? "";
|
|
74
|
+
const senderId = from ?? "anonymous";
|
|
75
|
+
const message = await this.router.routeMessage({
|
|
76
|
+
from: senderId,
|
|
77
|
+
to,
|
|
78
|
+
payload,
|
|
79
|
+
threadTag,
|
|
80
|
+
inReplyTo,
|
|
81
|
+
importance,
|
|
82
|
+
subject,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text" as const,
|
|
88
|
+
text: JSON.stringify({ ok: true, messageId: message.id }),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
this.mcp.tool(
|
|
96
|
+
"check_inbox",
|
|
97
|
+
"Check inbox for messages addressed to an agent. Auto-registers the agent if not already registered.",
|
|
98
|
+
{
|
|
99
|
+
agentId: z.string().describe("Agent ID to check inbox for"),
|
|
100
|
+
unreadOnly: z
|
|
101
|
+
.boolean()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Only return unread messages (default true)"),
|
|
104
|
+
limit: z
|
|
105
|
+
.number()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Max messages to return"),
|
|
108
|
+
},
|
|
109
|
+
async ({ agentId, unreadOnly, limit }) => {
|
|
110
|
+
// Auto-register the agent if not already known
|
|
111
|
+
this.ensureAgent(agentId);
|
|
112
|
+
|
|
113
|
+
const messages = this.storage.getInbox(agentId, {
|
|
114
|
+
unreadOnly: unreadOnly ?? true,
|
|
115
|
+
limit,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Auto-mark as read
|
|
119
|
+
for (const msg of messages) {
|
|
120
|
+
this.router.markRead(msg.id, agentId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text" as const,
|
|
127
|
+
text: JSON.stringify({
|
|
128
|
+
count: messages.length,
|
|
129
|
+
messages: messages.map((m) => ({
|
|
130
|
+
id: m.id,
|
|
131
|
+
from: m.sender_id,
|
|
132
|
+
subject: m.subject,
|
|
133
|
+
content: m.content,
|
|
134
|
+
threadTag: m.thread_tag,
|
|
135
|
+
importance: m.importance,
|
|
136
|
+
createdAt: m.created_at,
|
|
137
|
+
inReplyTo: m.in_reply_to,
|
|
138
|
+
})),
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
this.mcp.tool(
|
|
147
|
+
"read_thread",
|
|
148
|
+
"Read all messages in a thread (by thread_tag)",
|
|
149
|
+
{
|
|
150
|
+
threadTag: z.string().describe("Thread tag to read"),
|
|
151
|
+
scope: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Scope (defaults to 'default')"),
|
|
155
|
+
},
|
|
156
|
+
async ({ threadTag, scope }) => {
|
|
157
|
+
const messages = this.storage.getThread({
|
|
158
|
+
threadTag,
|
|
159
|
+
scope: scope ?? this.defaultScope,
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text" as const,
|
|
165
|
+
text: JSON.stringify({
|
|
166
|
+
threadTag,
|
|
167
|
+
count: messages.length,
|
|
168
|
+
messages: messages.map((m) => ({
|
|
169
|
+
id: m.id,
|
|
170
|
+
from: m.sender_id,
|
|
171
|
+
content: m.content,
|
|
172
|
+
createdAt: m.created_at,
|
|
173
|
+
inReplyTo: m.in_reply_to,
|
|
174
|
+
})),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
this.mcp.tool(
|
|
183
|
+
"list_agents",
|
|
184
|
+
"List agents registered in the inbox (local and optionally federated)",
|
|
185
|
+
{
|
|
186
|
+
scope: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Filter by scope"),
|
|
190
|
+
includeFederated: z
|
|
191
|
+
.boolean()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Include agents known from federation routing table"),
|
|
194
|
+
},
|
|
195
|
+
async ({ scope, includeFederated }) => {
|
|
196
|
+
const agents = this.storage.listAgents(scope);
|
|
197
|
+
const localList = agents.map((a) => ({
|
|
198
|
+
agentId: a.agent_id,
|
|
199
|
+
name: a.display_name,
|
|
200
|
+
scope: a.scope,
|
|
201
|
+
status: a.status,
|
|
202
|
+
program: a.program,
|
|
203
|
+
lastActive: a.last_active_at,
|
|
204
|
+
location: "local" as const,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
let federatedList: Array<{
|
|
208
|
+
agentId: string;
|
|
209
|
+
peerId: string;
|
|
210
|
+
status: string;
|
|
211
|
+
location: "federated";
|
|
212
|
+
}> = [];
|
|
213
|
+
if (includeFederated && this.router.getFederation()) {
|
|
214
|
+
const federation = this.router.getFederation()!;
|
|
215
|
+
const routingTable = federation.routing.getTable();
|
|
216
|
+
federatedList = routingTable.map((entry) => ({
|
|
217
|
+
agentId: entry.agentId,
|
|
218
|
+
peerId: entry.peerId,
|
|
219
|
+
status: entry.status,
|
|
220
|
+
location: "federated" as const,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: "text" as const,
|
|
228
|
+
text: JSON.stringify({
|
|
229
|
+
count: localList.length + federatedList.length,
|
|
230
|
+
agents: [...localList, ...federatedList],
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Auto-register an agent if not already known (lazy registration on first inbox check) */
|
|
240
|
+
private ensureAgent(agentId: string): void {
|
|
241
|
+
const existing = this.storage.listAgents().find(
|
|
242
|
+
(a) => a.agent_id === agentId
|
|
243
|
+
);
|
|
244
|
+
if (existing) return;
|
|
245
|
+
|
|
246
|
+
const now = new Date().toISOString();
|
|
247
|
+
const agent: Agent = {
|
|
248
|
+
agent_id: agentId,
|
|
249
|
+
scope: this.defaultScope,
|
|
250
|
+
status: "active",
|
|
251
|
+
metadata: {},
|
|
252
|
+
registered_at: now,
|
|
253
|
+
last_active_at: now,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (this.registry) {
|
|
257
|
+
this.registry.register(agent);
|
|
258
|
+
} else {
|
|
259
|
+
this.storage.putAgent(agent);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async start(): Promise<void> {
|
|
264
|
+
const transport = new StdioServerTransport();
|
|
265
|
+
await this.mcp.connect(transport);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Expose for testing — returns the underlying McpServer */
|
|
269
|
+
get server(): McpServer {
|
|
270
|
+
return this.mcp;
|
|
271
|
+
}
|
|
272
|
+
}
|