agent-inbox 0.0.1 → 0.1.1

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.
Files changed (126) hide show
  1. package/CLAUDE.md +113 -0
  2. package/README.md +195 -1
  3. package/dist/federation/address.d.ts +24 -0
  4. package/dist/federation/address.d.ts.map +1 -0
  5. package/dist/federation/address.js +54 -0
  6. package/dist/federation/address.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +118 -0
  8. package/dist/federation/connection-manager.d.ts.map +1 -0
  9. package/dist/federation/connection-manager.js +369 -0
  10. package/dist/federation/connection-manager.js.map +1 -0
  11. package/dist/federation/delivery-queue.d.ts +66 -0
  12. package/dist/federation/delivery-queue.d.ts.map +1 -0
  13. package/dist/federation/delivery-queue.js +199 -0
  14. package/dist/federation/delivery-queue.js.map +1 -0
  15. package/dist/federation/index.d.ts +7 -0
  16. package/dist/federation/index.d.ts.map +1 -0
  17. package/dist/federation/index.js +6 -0
  18. package/dist/federation/index.js.map +1 -0
  19. package/dist/federation/routing-engine.d.ts +74 -0
  20. package/dist/federation/routing-engine.d.ts.map +1 -0
  21. package/dist/federation/routing-engine.js +158 -0
  22. package/dist/federation/routing-engine.js.map +1 -0
  23. package/dist/federation/trust.d.ts +39 -0
  24. package/dist/federation/trust.d.ts.map +1 -0
  25. package/dist/federation/trust.js +64 -0
  26. package/dist/federation/trust.js.map +1 -0
  27. package/dist/index.d.ts +60 -2
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +217 -18
  30. package/dist/index.js.map +1 -1
  31. package/dist/ipc/ipc-server.d.ts +20 -0
  32. package/dist/ipc/ipc-server.d.ts.map +1 -0
  33. package/dist/ipc/ipc-server.js +152 -0
  34. package/dist/ipc/ipc-server.js.map +1 -0
  35. package/dist/jsonrpc/mail-server.d.ts +45 -0
  36. package/dist/jsonrpc/mail-server.d.ts.map +1 -0
  37. package/dist/jsonrpc/mail-server.js +284 -0
  38. package/dist/jsonrpc/mail-server.js.map +1 -0
  39. package/dist/map/map-client.d.ts +91 -0
  40. package/dist/map/map-client.d.ts.map +1 -0
  41. package/dist/map/map-client.js +202 -0
  42. package/dist/map/map-client.js.map +1 -0
  43. package/dist/mcp/mcp-server.d.ts +23 -0
  44. package/dist/mcp/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp/mcp-server.js +226 -0
  46. package/dist/mcp/mcp-server.js.map +1 -0
  47. package/dist/push/notifier.d.ts +49 -0
  48. package/dist/push/notifier.d.ts.map +1 -0
  49. package/dist/push/notifier.js +150 -0
  50. package/dist/push/notifier.js.map +1 -0
  51. package/dist/registry/warm-registry.d.ts +63 -0
  52. package/dist/registry/warm-registry.d.ts.map +1 -0
  53. package/dist/registry/warm-registry.js +173 -0
  54. package/dist/registry/warm-registry.js.map +1 -0
  55. package/dist/router/message-router.d.ts +44 -0
  56. package/dist/router/message-router.d.ts.map +1 -0
  57. package/dist/router/message-router.js +137 -0
  58. package/dist/router/message-router.js.map +1 -0
  59. package/dist/storage/interface.d.ts +31 -0
  60. package/dist/storage/interface.d.ts.map +1 -0
  61. package/dist/storage/interface.js +2 -0
  62. package/dist/storage/interface.js.map +1 -0
  63. package/dist/storage/memory.d.ts +28 -0
  64. package/dist/storage/memory.d.ts.map +1 -0
  65. package/dist/storage/memory.js +118 -0
  66. package/dist/storage/memory.js.map +1 -0
  67. package/dist/storage/sqlite.d.ts +35 -0
  68. package/dist/storage/sqlite.d.ts.map +1 -0
  69. package/dist/storage/sqlite.js +445 -0
  70. package/dist/storage/sqlite.js.map +1 -0
  71. package/dist/traceability/traceability.d.ts +29 -0
  72. package/dist/traceability/traceability.d.ts.map +1 -0
  73. package/dist/traceability/traceability.js +150 -0
  74. package/dist/traceability/traceability.js.map +1 -0
  75. package/dist/types.d.ts +253 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +3 -0
  78. package/dist/types.js.map +1 -0
  79. package/docs/DESIGN.md +1156 -0
  80. package/docs/PLAN.md +545 -0
  81. package/hooks/inbox-hook.mjs +119 -0
  82. package/hooks/register-hook.mjs +69 -0
  83. package/package.json +33 -25
  84. package/rules/agent-inbox.md +78 -0
  85. package/src/federation/address.ts +61 -0
  86. package/src/federation/connection-manager.ts +458 -0
  87. package/src/federation/delivery-queue.ts +222 -0
  88. package/src/federation/index.ts +6 -0
  89. package/src/federation/routing-engine.ts +188 -0
  90. package/src/federation/trust.ts +71 -0
  91. package/src/index.ts +299 -0
  92. package/src/ipc/ipc-server.ts +180 -0
  93. package/src/jsonrpc/mail-server.ts +356 -0
  94. package/src/map/map-client.ts +260 -0
  95. package/src/mcp/mcp-server.ts +272 -0
  96. package/src/push/notifier.ts +192 -0
  97. package/src/registry/warm-registry.ts +210 -0
  98. package/src/router/message-router.ts +175 -0
  99. package/src/storage/interface.ts +48 -0
  100. package/src/storage/memory.ts +145 -0
  101. package/src/storage/sqlite.ts +645 -0
  102. package/src/traceability/traceability.ts +183 -0
  103. package/src/types.ts +287 -0
  104. package/test/federation/address.test.ts +101 -0
  105. package/test/federation/connection-manager.test.ts +546 -0
  106. package/test/federation/delivery-queue.test.ts +159 -0
  107. package/test/federation/integration.test.ts +823 -0
  108. package/test/federation/routing-engine.test.ts +117 -0
  109. package/test/federation/sdk-integration.test.ts +748 -0
  110. package/test/federation/trust.test.ts +89 -0
  111. package/test/ipc-jsonrpc.test.ts +113 -0
  112. package/test/ipc-server.test.ts +138 -0
  113. package/test/mail-server.test.ts +208 -0
  114. package/test/map-client.test.ts +408 -0
  115. package/test/message-router.test.ts +184 -0
  116. package/test/push-notifier.test.ts +139 -0
  117. package/test/registry/warm-registry.test.ts +171 -0
  118. package/test/sqlite-storage.test.ts +243 -0
  119. package/test/storage.test.ts +196 -0
  120. package/test/traceability.test.ts +123 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +10 -0
  123. package/vitest.config.ts +8 -0
  124. package/dist/index.d.mts +0 -2
  125. package/dist/index.mjs +0 -1
  126. 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
+ }