@oobe-protocol-labs/sap-mcp-server 0.1.1 → 0.2.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 (97) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +21 -2
  3. package/USER_DOCS/00_START_HERE.md +130 -0
  4. package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +106 -0
  5. package/USER_DOCS/02_LOCAL_STDIO_MCP.md +91 -0
  6. package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +212 -0
  7. package/USER_DOCS/04_CLIENT_CONFIGS.md +73 -0
  8. package/USER_DOCS/05_SKILLS_AND_TOOLS.md +81 -0
  9. package/assets/explorer_logo.png +0 -0
  10. package/dist/bin/sap-mcp-remote.js +2 -1
  11. package/dist/bin/sap-mcp-remote.js.map +1 -1
  12. package/dist/config/env.d.ts +14 -14
  13. package/dist/config/env.d.ts.map +1 -1
  14. package/dist/config/env.js +30 -7
  15. package/dist/config/env.js.map +1 -1
  16. package/dist/core/constants.d.ts +1 -1
  17. package/dist/core/constants.js +1 -1
  18. package/dist/core/logger.d.ts +5 -0
  19. package/dist/core/logger.d.ts.map +1 -1
  20. package/dist/core/logger.js +61 -3
  21. package/dist/core/logger.js.map +1 -1
  22. package/dist/payments/monetization-gate.d.ts +6 -1
  23. package/dist/payments/monetization-gate.d.ts.map +1 -1
  24. package/dist/payments/monetization-gate.js +25 -8
  25. package/dist/payments/monetization-gate.js.map +1 -1
  26. package/dist/payments/oobe-facilitator-server.js +2 -2
  27. package/dist/payments/oobe-facilitator-server.js.map +1 -1
  28. package/dist/payments/pay-sh-spec.d.ts.map +1 -1
  29. package/dist/payments/pay-sh-spec.js +2 -1
  30. package/dist/payments/pay-sh-spec.js.map +1 -1
  31. package/dist/payments/usage-ledger.d.ts +9 -1
  32. package/dist/payments/usage-ledger.d.ts.map +1 -1
  33. package/dist/payments/usage-ledger.js +29 -1
  34. package/dist/payments/usage-ledger.js.map +1 -1
  35. package/dist/prompts/context/sap-agent-context.prompt.js +2 -2
  36. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  37. package/dist/remote/rate-limiter.d.ts +57 -0
  38. package/dist/remote/rate-limiter.d.ts.map +1 -0
  39. package/dist/remote/rate-limiter.js +127 -0
  40. package/dist/remote/rate-limiter.js.map +1 -0
  41. package/dist/remote/server.d.ts +68 -0
  42. package/dist/remote/server.d.ts.map +1 -1
  43. package/dist/remote/server.js +306 -19
  44. package/dist/remote/server.js.map +1 -1
  45. package/dist/resources/current/sap-current-config.resource.js +2 -2
  46. package/dist/resources/current/sap-current-config.resource.js.map +1 -1
  47. package/dist/resources/stats/sap-network-stats.resource.js +2 -2
  48. package/dist/resources/stats/sap-network-stats.resource.js.map +1 -1
  49. package/dist/security/tool-permissions.d.ts.map +1 -1
  50. package/dist/security/tool-permissions.js +25 -0
  51. package/dist/security/tool-permissions.js.map +1 -1
  52. package/dist/server/server-metadata.d.ts +1 -0
  53. package/dist/server/server-metadata.d.ts.map +1 -1
  54. package/dist/server/server-metadata.js +3 -2
  55. package/dist/server/server-metadata.js.map +1 -1
  56. package/dist/tools/chat-tools.d.ts +14 -0
  57. package/dist/tools/chat-tools.d.ts.map +1 -0
  58. package/dist/tools/chat-tools.js +697 -0
  59. package/dist/tools/chat-tools.js.map +1 -0
  60. package/dist/tools/client-sdk-tools.d.ts.map +1 -1
  61. package/dist/tools/client-sdk-tools.js +178 -2
  62. package/dist/tools/client-sdk-tools.js.map +1 -1
  63. package/dist/tools/index.d.ts +1 -0
  64. package/dist/tools/index.d.ts.map +1 -1
  65. package/dist/tools/index.js +2 -0
  66. package/dist/tools/index.js.map +1 -1
  67. package/dist/tools/profile-tools.d.ts.map +1 -1
  68. package/dist/tools/profile-tools.js +3 -2
  69. package/dist/tools/profile-tools.js.map +1 -1
  70. package/dist/tools/register-tools.d.ts.map +1 -1
  71. package/dist/tools/register-tools.js +3 -0
  72. package/dist/tools/register-tools.js.map +1 -1
  73. package/dist/tools/sap-network-stats.tool.js +2 -2
  74. package/dist/tools/sap-network-stats.tool.js.map +1 -1
  75. package/dist/tools/sap-sdk-tools.d.ts.map +1 -1
  76. package/dist/tools/sap-sdk-tools.js +34 -1
  77. package/dist/tools/sap-sdk-tools.js.map +1 -1
  78. package/dist/transports/http.js +3 -3
  79. package/dist/transports/http.js.map +1 -1
  80. package/docs/00_README.md +4 -0
  81. package/docs/01_PRODUCT_OVERVIEW.md +2 -1
  82. package/docs/02_ARCHITECTURE_AND_REQUEST_FLOW.md +1 -1
  83. package/docs/05_REMOTE_VPS_DEPLOYMENT.md +13 -0
  84. package/docs/07_ENDPOINTS_AND_CLIENTS.md +6 -1
  85. package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +2 -1
  86. package/docs/10_OPERATIONS_RELEASE_AND_PM2.md +13 -2
  87. package/docs/12_ONCHAIN_AGENT_CHAT.md +178 -0
  88. package/package.json +17 -14
  89. package/skills/README.md +1 -0
  90. package/skills/sap-agent-chat/SKILL.md +71 -0
  91. package/skills/sap-mcp/SKILL.md +3 -2
  92. package/vendor/bigint-buffer/LICENSE +1 -0
  93. package/vendor/bigint-buffer/README.md +8 -0
  94. package/vendor/bigint-buffer/dist/browser.js +3 -0
  95. package/vendor/bigint-buffer/dist/index.d.ts +35 -0
  96. package/vendor/bigint-buffer/dist/node.js +62 -0
  97. package/vendor/bigint-buffer/package.json +20 -0
@@ -0,0 +1,697 @@
1
+ /**
2
+ * @module chat-tools
3
+ * @description MCP tools for SAP on-chain chat rooms backed by SAP session ledgers.
4
+ */
5
+ import { createHash } from 'crypto';
6
+ import { registerTool } from '../adapters/mcp/sdk-compat.js';
7
+ import { createTextResponse } from '../adapters/mcp/tool-response.js';
8
+ const CHAT_PROTOCOL_PREFIX = 'sap-chat:v1';
9
+ const MAX_WIRE_BYTES = 700;
10
+ const DEFAULT_CHUNK_BYTES = 420;
11
+ /**
12
+ * @name registerChatTools
13
+ * @description Registers SAP chat tools backed by the SDK SessionManager and memory ledger.
14
+ * @param server - MCP server receiving tool definitions.
15
+ * @param context - Runtime context with SAP client and policy engine.
16
+ */
17
+ export function registerChatTools(server, context) {
18
+ registerTool(server, 'sap_chat_derive_room', {
19
+ description: 'Derive a deterministic SAP chat room/session ID. Group and public room IDs are active; DM derivation is reserved for future native support.',
20
+ inputSchema: chatRoomInputSchema(),
21
+ }, async (input) => ok({
22
+ room: deriveRoomDescriptor(asRecord(input), context.sapClient),
23
+ }));
24
+ registerTool(server, 'sap_chat_start_room', {
25
+ description: 'Start an on-chain SAP chat room by creating the backing vault, session, and ledger if needed.',
26
+ inputSchema: chatRoomInputSchema(),
27
+ }, async (input) => {
28
+ const room = deriveRoomDescriptor(asRecord(input), context.sapClient);
29
+ assertGroupChatEnabled(room);
30
+ const session = await context.sapClient.session.start(room.sessionId);
31
+ return ok({ room, session: serializeSessionContext(session) });
32
+ });
33
+ registerTool(server, 'sap_chat_send_message', {
34
+ description: 'Send a chunked SAP chat message to a room. Public messages store UTF-8 text; private messages store caller-provided ciphertext bytes.',
35
+ inputSchema: {
36
+ ...chatRoomInputSchema(),
37
+ content: { type: 'string', description: 'Plain UTF-8 message content for public messages.' },
38
+ payloadBase64: { type: 'string', description: 'Base64 payload. Required for private/ciphertext messages.' },
39
+ visibility: { type: 'string', enum: ['public', 'private'], description: 'Message visibility. Defaults to public.' },
40
+ contentType: { type: 'string', description: 'MIME content type. Defaults to text/plain for public messages.' },
41
+ replyTo: { type: 'string', description: 'Optional parent message ID.' },
42
+ topic: { type: 'string', description: 'Required thematic group topic when deriving or writing to topic-scoped groups.' },
43
+ policy: { type: 'object', description: 'Optional compact group policy reference or policy hash. Full policy bodies should live off-chain and be linked by hash.' },
44
+ links: { type: 'array', items: { type: 'object' }, description: 'Optional signed link references. Each link must include url and may include kind, label, sha256.' },
45
+ metadata: { type: 'object', description: 'Optional JSON metadata. Keep small; large metadata should be stored off-chain and hashed.' },
46
+ },
47
+ }, async (input) => {
48
+ const record = asRecord(input);
49
+ const room = deriveRoomDescriptor(record, context.sapClient);
50
+ assertGroupChatEnabled(room);
51
+ const session = await context.sapClient.session.start(room.sessionId);
52
+ const envelopes = buildMessageEnvelopes(record, room, session);
53
+ const signedWriteProofs = [];
54
+ for (const envelope of envelopes) {
55
+ signedWriteProofs.push(await context.sapClient.session.write(session, JSON.stringify(envelope)));
56
+ }
57
+ return ok({
58
+ room,
59
+ message: summarizeEnvelopeSet(envelopes),
60
+ signedWriteProofs,
61
+ });
62
+ });
63
+ registerTool(server, 'sap_chat_publish_manifest', {
64
+ description: 'Publish a signed thematic room/group manifest for discovery indexers, policy-aware agents, and chat clients.',
65
+ inputSchema: {
66
+ ...chatRoomInputSchema(),
67
+ description: { type: 'string', description: 'Short public room/group description.' },
68
+ tags: { type: 'array', items: { type: 'string' }, description: 'Small topic tags used by discovery, for example openbook, markets, sol-usdc.' },
69
+ policy: { type: 'object', description: 'Compact group policy or content-addressed policy reference.' },
70
+ links: { type: 'array', items: { type: 'object' }, description: 'Optional signed room links for docs, market references, IPFS manifests, or execution receipts.' },
71
+ metadata: { type: 'object', description: 'Optional compact manifest metadata.' },
72
+ },
73
+ }, async (input) => {
74
+ const record = asRecord(input);
75
+ const room = deriveRoomDescriptor(record, context.sapClient);
76
+ assertGroupChatEnabled(room);
77
+ const session = await context.sapClient.session.start(room.sessionId);
78
+ const manifest = buildRoomManifestEnvelopes(record, room, session);
79
+ const signedWriteProofs = [];
80
+ for (const envelope of manifest) {
81
+ signedWriteProofs.push(await context.sapClient.session.write(session, JSON.stringify(envelope)));
82
+ }
83
+ return ok({
84
+ room,
85
+ manifest: summarizeEnvelopeSet(manifest),
86
+ signedWriteProofs,
87
+ });
88
+ });
89
+ registerTool(server, 'sap_chat_read_latest', {
90
+ description: 'Read latest SAP chat messages from the room ring buffer.',
91
+ inputSchema: chatRoomInputSchema(),
92
+ }, async (input) => {
93
+ const room = deriveRoomDescriptor(asRecord(input), context.sapClient);
94
+ assertGroupChatEnabled(room);
95
+ const session = context.sapClient.session.deriveContext(room.sessionId);
96
+ const entries = await context.sapClient.session.readLatest(session);
97
+ return ok({ room, messages: decodeChatMessages(entries) });
98
+ });
99
+ registerTool(server, 'sap_chat_read_all', {
100
+ description: 'Read all SAP chat messages from sealed ledger pages plus the latest ring buffer.',
101
+ inputSchema: chatRoomInputSchema(),
102
+ }, async (input) => {
103
+ const room = deriveRoomDescriptor(asRecord(input), context.sapClient);
104
+ assertGroupChatEnabled(room);
105
+ const session = context.sapClient.session.deriveContext(room.sessionId);
106
+ const entries = await context.sapClient.session.readAll(session);
107
+ return ok({ room, messages: decodeChatMessages(entries) });
108
+ });
109
+ registerTool(server, 'sap_chat_status', {
110
+ description: 'Get backing SAP memory session status for a chat room.',
111
+ inputSchema: chatRoomInputSchema(),
112
+ }, async (input) => {
113
+ const room = deriveRoomDescriptor(asRecord(input), context.sapClient);
114
+ assertGroupChatEnabled(room);
115
+ const session = context.sapClient.session.deriveContext(room.sessionId);
116
+ return ok({ room, status: await context.sapClient.session.getStatus(session) });
117
+ });
118
+ registerTool(server, 'sap_chat_seal_room', {
119
+ description: 'Seal the current chat ring buffer into an immutable SAP ledger page for permanent history.',
120
+ inputSchema: chatRoomInputSchema(),
121
+ }, async (input) => {
122
+ const room = deriveRoomDescriptor(asRecord(input), context.sapClient);
123
+ assertGroupChatEnabled(room);
124
+ const session = context.sapClient.session.deriveContext(room.sessionId);
125
+ return ok({ room, seal: await context.sapClient.session.seal(session) });
126
+ });
127
+ }
128
+ /**
129
+ * @name chatRoomInputSchema
130
+ * @description Builds the shared JSON schema for chat room selection.
131
+ * @returns JSON-schema-like MCP input properties.
132
+ */
133
+ function chatRoomInputSchema() {
134
+ return {
135
+ roomKind: { type: 'string', enum: ['dm', 'group', 'room'], description: 'Chat room kind: dm, group, or room.' },
136
+ roomId: { type: 'string', description: 'Optional explicit room ID. If omitted, one is derived deterministically.' },
137
+ roomName: { type: 'string', description: 'Human-readable room name for public thematic rooms.' },
138
+ topic: { type: 'string', description: 'Thematic topic, for example openbook:sol-usdc:market-makers or sap:registry:discovery.' },
139
+ participants: { type: 'array', items: { type: 'string' }, description: 'Participant agent/wallet identifiers for DM or group rooms.' },
140
+ };
141
+ }
142
+ /**
143
+ * @name deriveRoomDescriptor
144
+ * @description Derives a stable SAP chat room descriptor from tool input and signer identity.
145
+ */
146
+ function deriveRoomDescriptor(input, client) {
147
+ const roomKind = readRoomKind(input);
148
+ const sender = senderIdentity(client);
149
+ const participants = normalizeParticipants(input.participants, sender.wallet);
150
+ const explicitRoomId = readOptionalString(input, 'roomId');
151
+ const roomName = readOptionalString(input, 'roomName');
152
+ const topic = readOptionalString(input, 'topic') ?? readOptionalString(readOptionalRecord(input, 'metadata') ?? {}, 'topic');
153
+ if (roomKind === 'dm' && participants.length !== 2) {
154
+ throw new Error('dm rooms require exactly two unique participants including the signer wallet');
155
+ }
156
+ if (roomKind === 'group' && participants.length < 2) {
157
+ throw new Error('group rooms require at least two unique participants');
158
+ }
159
+ if (roomKind === 'room' && !explicitRoomId && !roomName) {
160
+ throw new Error('roomName or roomId is required for public rooms');
161
+ }
162
+ const seed = explicitRoomId ?? (roomKind === 'room'
163
+ ? `room:${roomName}`
164
+ : `${roomKind}:${topic ?? 'general'}:${participants.join(',')}`);
165
+ const roomId = stableId(seed);
166
+ return {
167
+ roomId,
168
+ roomKind,
169
+ sessionId: `${CHAT_PROTOCOL_PREFIX}:${roomKind}:${roomId}`,
170
+ participants,
171
+ ...(topic ? { topic } : {}),
172
+ };
173
+ }
174
+ /**
175
+ * @name buildMessageEnvelopes
176
+ * @description Builds one or more wire-safe chat envelopes from a message payload.
177
+ */
178
+ function buildMessageEnvelopes(input, room, session) {
179
+ const visibility = readVisibility(input);
180
+ const contentType = readOptionalString(input, 'contentType') ?? (visibility === 'private' ? 'application/octet-stream' : 'text/plain');
181
+ const replyTo = readOptionalString(input, 'replyTo');
182
+ const metadata = buildMessageMetadata(input, room);
183
+ const payloadEncoding = visibility === 'private'
184
+ ? 'ciphertext'
185
+ : input.payloadBase64 ? 'base64' : 'utf8';
186
+ const payload = readPayload(input, visibility);
187
+ const payloadHash = sha256Hex(payload);
188
+ const createdAt = new Date().toISOString();
189
+ const messageId = stableId(`${room.sessionId}:${session.wallet.toBase58()}:${createdAt}:${payloadHash}`);
190
+ const sender = senderIdentityFromContext(session);
191
+ const chunks = chunkBytes(payload, DEFAULT_CHUNK_BYTES);
192
+ return chunks.map((chunk, index) => {
193
+ const envelope = {
194
+ v: 1,
195
+ protocol: 'sap.chat',
196
+ type: 'message',
197
+ roomId: room.roomId,
198
+ roomKind: room.roomKind,
199
+ sessionId: room.sessionId,
200
+ messageId,
201
+ senderAgent: sender.agent,
202
+ senderWallet: sender.wallet,
203
+ createdAt,
204
+ visibility,
205
+ contentType,
206
+ payloadEncoding,
207
+ chunk: {
208
+ index,
209
+ total: chunks.length,
210
+ payloadHash,
211
+ },
212
+ body: Buffer.from(chunk).toString('base64'),
213
+ ...(replyTo ? { replyTo } : {}),
214
+ ...(room.participants.length > 0 ? { participants: room.participants } : {}),
215
+ ...(metadata ? { metadata } : {}),
216
+ };
217
+ const byteLength = Buffer.byteLength(JSON.stringify(envelope), 'utf8');
218
+ if (byteLength > MAX_WIRE_BYTES) {
219
+ throw new Error(`Chat envelope is ${byteLength} bytes; reduce content or metadata to stay below ${MAX_WIRE_BYTES} bytes`);
220
+ }
221
+ return envelope;
222
+ });
223
+ }
224
+ /**
225
+ * @name buildRoomManifestEnvelopes
226
+ * @description Builds a signed room manifest payload for discovery and group policy indexing.
227
+ */
228
+ function buildRoomManifestEnvelopes(input, room, session) {
229
+ const description = readOptionalString(input, 'description') ?? '';
230
+ const tags = readTags(input.tags);
231
+ const topic = readOptionalString(input, 'topic') ?? room.topic ?? 'general';
232
+ const policy = readOptionalRecord(input, 'policy') ?? readOptionalRecord(readOptionalRecord(input, 'metadata') ?? {}, 'policy');
233
+ const links = readLinks(input.links ?? readOptionalRecord(input, 'metadata')?.links);
234
+ const manifest = {
235
+ kind: 'sap.chat.room_manifest',
236
+ topic,
237
+ description,
238
+ tags,
239
+ participants: room.participants,
240
+ discovery: {
241
+ scope: 'public-index',
242
+ key: `${topic}:${room.roomId}`,
243
+ },
244
+ ...(policy ? { policy } : {}),
245
+ ...(links.length > 0 ? { links: linksToJson(links) } : {}),
246
+ };
247
+ const payload = Buffer.from(JSON.stringify(manifest), 'utf8');
248
+ const payloadHash = sha256Hex(payload);
249
+ const createdAt = new Date().toISOString();
250
+ const sender = senderIdentityFromContext(session);
251
+ const chunks = chunkBytes(payload, DEFAULT_CHUNK_BYTES);
252
+ const messageId = stableId(`${room.sessionId}:manifest:${sender.wallet}:${createdAt}:${payloadHash}`);
253
+ return chunks.map((chunk, index) => {
254
+ const envelope = {
255
+ v: 1,
256
+ protocol: 'sap.chat',
257
+ type: 'room_manifest',
258
+ roomId: room.roomId,
259
+ roomKind: room.roomKind,
260
+ sessionId: room.sessionId,
261
+ messageId,
262
+ senderAgent: sender.agent,
263
+ senderWallet: sender.wallet,
264
+ createdAt,
265
+ visibility: 'public',
266
+ contentType: 'application/sap-chat-manifest+json',
267
+ payloadEncoding: 'utf8',
268
+ chunk: {
269
+ index,
270
+ total: chunks.length,
271
+ payloadHash,
272
+ },
273
+ body: Buffer.from(chunk).toString('base64'),
274
+ ...(room.participants.length > 0 ? { participants: room.participants } : {}),
275
+ metadata: {
276
+ topic,
277
+ tags,
278
+ discoveryKey: `${topic}:${room.roomId}`,
279
+ ...(policy ? { policy } : {}),
280
+ ...(links.length > 0 ? { links: linksToJson(links) } : {}),
281
+ },
282
+ };
283
+ const byteLength = Buffer.byteLength(JSON.stringify(envelope), 'utf8');
284
+ if (byteLength > MAX_WIRE_BYTES) {
285
+ throw new Error(`Chat manifest envelope is ${byteLength} bytes; reduce description, tags, links, or policy metadata to stay below ${MAX_WIRE_BYTES} bytes`);
286
+ }
287
+ return envelope;
288
+ });
289
+ }
290
+ /**
291
+ * @name assertGroupChatEnabled
292
+ * @description Prevents operational DM usage while preserving DM derivation for future native support.
293
+ */
294
+ function assertGroupChatEnabled(room) {
295
+ if (room.roomKind === 'dm') {
296
+ throw new Error('Direct messages are reserved for future native support. SAP MCP currently enables signed thematic group and public room chat only.');
297
+ }
298
+ }
299
+ /**
300
+ * @name buildMessageMetadata
301
+ * @description Merges compact policy, topic, and signed link metadata into a wire-safe JSON record.
302
+ */
303
+ function buildMessageMetadata(input, room) {
304
+ const base = readOptionalRecord(input, 'metadata') ?? {};
305
+ const topic = readOptionalString(input, 'topic') ?? room.topic ?? readOptionalString(base, 'topic');
306
+ const policy = readOptionalRecord(input, 'policy') ?? readOptionalRecord(base, 'policy');
307
+ const links = readLinks(input.links ?? base.links);
308
+ const next = { ...base };
309
+ if (topic) {
310
+ next.topic = topic;
311
+ }
312
+ if (policy) {
313
+ next.policy = policy;
314
+ }
315
+ if (links.length > 0) {
316
+ next.links = linksToJson(links);
317
+ }
318
+ return Object.keys(next).length > 0 ? next : undefined;
319
+ }
320
+ /**
321
+ * @name decodeChatMessages
322
+ * @description Decodes ledger entries into assembled chat messages.
323
+ */
324
+ function decodeChatMessages(entries) {
325
+ const envelopes = entries
326
+ .map((entry) => parseEnvelope(entry.text))
327
+ .filter((envelope) => envelope !== null);
328
+ const byMessage = new Map();
329
+ for (const envelope of envelopes) {
330
+ const group = byMessage.get(envelope.messageId) ?? [];
331
+ group.push(envelope);
332
+ byMessage.set(envelope.messageId, group);
333
+ }
334
+ return Array.from(byMessage.values())
335
+ .map(assembleMessage)
336
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt));
337
+ }
338
+ /**
339
+ * @name assembleMessage
340
+ * @description Reassembles chunked envelopes into a single chat message result.
341
+ */
342
+ function assembleMessage(envelopes) {
343
+ const ordered = [...envelopes].sort((left, right) => left.chunk.index - right.chunk.index);
344
+ const first = ordered[0];
345
+ if (!first) {
346
+ throw new Error('Cannot assemble an empty envelope set');
347
+ }
348
+ const payload = Buffer.concat(ordered.map((envelope) => Buffer.from(envelope.body, 'base64')));
349
+ const complete = ordered.length === first.chunk.total && sha256Hex(payload) === first.chunk.payloadHash;
350
+ const base = {
351
+ type: first.type,
352
+ roomId: first.roomId,
353
+ roomKind: first.roomKind,
354
+ sessionId: first.sessionId,
355
+ messageId: first.messageId,
356
+ senderAgent: first.senderAgent,
357
+ senderWallet: first.senderWallet,
358
+ createdAt: first.createdAt,
359
+ visibility: first.visibility,
360
+ contentType: first.contentType,
361
+ payloadEncoding: first.payloadEncoding,
362
+ payloadHash: first.chunk.payloadHash,
363
+ complete,
364
+ chunksReceived: ordered.length,
365
+ totalChunks: first.chunk.total,
366
+ ...(first.replyTo ? { replyTo: first.replyTo } : {}),
367
+ ...(first.participants ? { participants: first.participants } : {}),
368
+ ...(first.metadata ? { metadata: first.metadata } : {}),
369
+ };
370
+ if (first.visibility === 'public' && first.payloadEncoding === 'utf8') {
371
+ return { ...base, text: payload.toString('utf8') };
372
+ }
373
+ return { ...base, payloadBase64: payload.toString('base64') };
374
+ }
375
+ /**
376
+ * @name parseEnvelope
377
+ * @description Parses one ledger text entry as a SAP chat envelope.
378
+ */
379
+ function parseEnvelope(text) {
380
+ try {
381
+ const parsed = JSON.parse(text);
382
+ if (!isChatEnvelope(parsed)) {
383
+ return null;
384
+ }
385
+ return parsed;
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ }
391
+ /**
392
+ * @name isChatEnvelope
393
+ * @description Validates the stable fields required to reassemble a SAP chat envelope.
394
+ */
395
+ function isChatEnvelope(value) {
396
+ if (!isRecord(value)) {
397
+ return false;
398
+ }
399
+ const chunk = value.chunk;
400
+ return value.v === 1
401
+ && value.protocol === 'sap.chat'
402
+ && (value.type === 'message' || value.type === 'room_manifest')
403
+ && isRoomKind(value.roomKind)
404
+ && isVisibility(value.visibility)
405
+ && isPayloadEncoding(value.payloadEncoding)
406
+ && typeof value.roomId === 'string'
407
+ && typeof value.sessionId === 'string'
408
+ && typeof value.messageId === 'string'
409
+ && typeof value.senderAgent === 'string'
410
+ && typeof value.senderWallet === 'string'
411
+ && typeof value.createdAt === 'string'
412
+ && typeof value.contentType === 'string'
413
+ && typeof value.body === 'string'
414
+ && isRecord(chunk)
415
+ && typeof chunk.index === 'number'
416
+ && Number.isInteger(chunk.index)
417
+ && chunk.index >= 0
418
+ && typeof chunk.total === 'number'
419
+ && Number.isInteger(chunk.total)
420
+ && chunk.total > 0
421
+ && typeof chunk.payloadHash === 'string'
422
+ && (value.replyTo === undefined || typeof value.replyTo === 'string')
423
+ && (value.participants === undefined || isStringArray(value.participants))
424
+ && (value.metadata === undefined || isRecord(value.metadata));
425
+ }
426
+ /**
427
+ * @name readPayload
428
+ * @description Reads public UTF-8 content or private base64 ciphertext from tool input.
429
+ */
430
+ function readPayload(input, visibility) {
431
+ const payloadBase64 = readOptionalString(input, 'payloadBase64');
432
+ if (payloadBase64) {
433
+ return Buffer.from(payloadBase64, 'base64');
434
+ }
435
+ if (visibility === 'private') {
436
+ throw new Error('private chat messages require payloadBase64 containing ciphertext');
437
+ }
438
+ const content = readRequiredString(input, 'content');
439
+ return Buffer.from(content, 'utf8');
440
+ }
441
+ /**
442
+ * @name summarizeEnvelopeSet
443
+ * @description Builds a compact message summary after on-chain writes.
444
+ */
445
+ function summarizeEnvelopeSet(envelopes) {
446
+ const first = envelopes[0];
447
+ if (!first) {
448
+ throw new Error('No chat envelopes were created');
449
+ }
450
+ return {
451
+ roomId: first.roomId,
452
+ type: first.type,
453
+ roomKind: first.roomKind,
454
+ sessionId: first.sessionId,
455
+ messageId: first.messageId,
456
+ senderAgent: first.senderAgent,
457
+ senderWallet: first.senderWallet,
458
+ createdAt: first.createdAt,
459
+ visibility: first.visibility,
460
+ contentType: first.contentType,
461
+ payloadEncoding: first.payloadEncoding,
462
+ payloadHash: first.chunk.payloadHash,
463
+ totalChunks: first.chunk.total,
464
+ ...(first.replyTo ? { replyTo: first.replyTo } : {}),
465
+ ...(first.participants ? { participants: first.participants } : {}),
466
+ ...(first.metadata ? { metadata: first.metadata } : {}),
467
+ };
468
+ }
469
+ /**
470
+ * @name serializeSessionContext
471
+ * @description Converts SDK session context PublicKeys into JSON-safe strings.
472
+ */
473
+ function serializeSessionContext(session) {
474
+ return {
475
+ sessionId: session.sessionId,
476
+ sessionHash: Buffer.from(session.sessionHash).toString('hex'),
477
+ agentPda: session.agentPda.toBase58(),
478
+ vaultPda: session.vaultPda.toBase58(),
479
+ sessionPda: session.sessionPda.toBase58(),
480
+ ledgerPda: session.ledgerPda.toBase58(),
481
+ wallet: session.wallet.toBase58(),
482
+ };
483
+ }
484
+ /**
485
+ * @name senderIdentity
486
+ * @description Derives sender wallet and agent identity from a SAP client.
487
+ */
488
+ function senderIdentity(client) {
489
+ const ctx = client.session.deriveContext(`${CHAT_PROTOCOL_PREFIX}:identity`);
490
+ return senderIdentityFromContext(ctx);
491
+ }
492
+ /**
493
+ * @name senderIdentityFromContext
494
+ * @description Reads signer wallet and agent PDA from a session context.
495
+ */
496
+ function senderIdentityFromContext(ctx) {
497
+ return {
498
+ wallet: ctx.wallet.toBase58(),
499
+ agent: ctx.agentPda.toBase58(),
500
+ };
501
+ }
502
+ /**
503
+ * @name normalizeParticipants
504
+ * @description Builds a sorted unique participant list and always includes the signer wallet.
505
+ */
506
+ function normalizeParticipants(value, signerWallet) {
507
+ const input = Array.isArray(value)
508
+ ? value.filter((item) => typeof item === 'string')
509
+ : [];
510
+ return Array.from(new Set([...input, signerWallet].filter(Boolean))).sort();
511
+ }
512
+ /**
513
+ * @name chunkBytes
514
+ * @description Splits payload bytes into deterministic chunks.
515
+ */
516
+ function chunkBytes(bytes, chunkSize) {
517
+ if (bytes.length === 0) {
518
+ return [Buffer.alloc(0)];
519
+ }
520
+ const chunks = [];
521
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
522
+ chunks.push(bytes.subarray(offset, offset + chunkSize));
523
+ }
524
+ return chunks;
525
+ }
526
+ /**
527
+ * @name stableId
528
+ * @description Builds a stable short identifier from arbitrary text.
529
+ */
530
+ function stableId(value) {
531
+ return sha256Hex(Buffer.from(value, 'utf8')).slice(0, 32);
532
+ }
533
+ /**
534
+ * @name sha256Hex
535
+ * @description Computes SHA-256 as lowercase hex.
536
+ */
537
+ function sha256Hex(value) {
538
+ return createHash('sha256').update(value).digest('hex');
539
+ }
540
+ /**
541
+ * @name ok
542
+ * @description Wraps a JSON payload in the MCP text response shape.
543
+ */
544
+ function ok(payload) {
545
+ return createTextResponse(JSON.stringify({ success: true, ...payload }, null, 2));
546
+ }
547
+ /**
548
+ * @name asRecord
549
+ * @description Normalizes unknown MCP input into a JSON record.
550
+ */
551
+ function asRecord(value) {
552
+ return isRecord(value) ? value : {};
553
+ }
554
+ /**
555
+ * @name isRecord
556
+ * @description Checks whether a value is a non-array object.
557
+ */
558
+ function isRecord(value) {
559
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
560
+ }
561
+ /**
562
+ * @name readRoomKind
563
+ * @description Reads and validates the chat room kind.
564
+ */
565
+ function readRoomKind(input) {
566
+ const value = input.roomKind;
567
+ if (isRoomKind(value)) {
568
+ return value;
569
+ }
570
+ return 'room';
571
+ }
572
+ /**
573
+ * @name readVisibility
574
+ * @description Reads and validates chat visibility.
575
+ */
576
+ function readVisibility(input) {
577
+ return input.visibility === 'private' ? 'private' : 'public';
578
+ }
579
+ /**
580
+ * @name isRoomKind
581
+ * @description Checks whether a value is a supported chat room kind.
582
+ */
583
+ function isRoomKind(value) {
584
+ return value === 'dm' || value === 'group' || value === 'room';
585
+ }
586
+ /**
587
+ * @name isVisibility
588
+ * @description Checks whether a value is a supported chat visibility.
589
+ */
590
+ function isVisibility(value) {
591
+ return value === 'public' || value === 'private';
592
+ }
593
+ /**
594
+ * @name isPayloadEncoding
595
+ * @description Checks whether a value is a supported chat payload encoding.
596
+ */
597
+ function isPayloadEncoding(value) {
598
+ return value === 'utf8' || value === 'base64' || value === 'ciphertext';
599
+ }
600
+ /**
601
+ * @name isStringArray
602
+ * @description Checks whether a value is an array of strings.
603
+ */
604
+ function isStringArray(value) {
605
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
606
+ }
607
+ /**
608
+ * @name readRequiredString
609
+ * @description Reads a required string field from a JSON record.
610
+ */
611
+ function readRequiredString(input, field) {
612
+ const value = input[field];
613
+ if (typeof value !== 'string' || value.length === 0) {
614
+ throw new Error(`${field} is required`);
615
+ }
616
+ return value;
617
+ }
618
+ /**
619
+ * @name readOptionalString
620
+ * @description Reads an optional string field from a JSON record.
621
+ */
622
+ function readOptionalString(input, field) {
623
+ const value = input[field];
624
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
625
+ }
626
+ /**
627
+ * @name readOptionalRecord
628
+ * @description Reads an optional JSON object field.
629
+ */
630
+ function readOptionalRecord(input, field) {
631
+ const value = input[field];
632
+ return isRecord(value) ? value : undefined;
633
+ }
634
+ /**
635
+ * @name readLinks
636
+ * @description Reads and validates signed link references included in a group message.
637
+ */
638
+ function readLinks(value) {
639
+ if (!Array.isArray(value)) {
640
+ return [];
641
+ }
642
+ return value.map((item) => {
643
+ if (!isRecord(item) || typeof item.url !== 'string') {
644
+ throw new Error('Each chat link must be an object with a url string');
645
+ }
646
+ const url = new URL(item.url);
647
+ if (url.protocol !== 'https:' && url.protocol !== 'ipfs:' && url.protocol !== 'ar:') {
648
+ throw new Error('Chat links must use https://, ipfs://, or ar:// URLs');
649
+ }
650
+ const kind = readLinkKind(item.kind);
651
+ const label = typeof item.label === 'string' && item.label.length > 0 ? item.label : undefined;
652
+ const sha256 = typeof item.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(item.sha256) ? item.sha256.toLowerCase() : undefined;
653
+ return {
654
+ kind,
655
+ url: item.url,
656
+ ...(label ? { label } : {}),
657
+ ...(sha256 ? { sha256 } : {}),
658
+ };
659
+ });
660
+ }
661
+ /**
662
+ * @name linksToJson
663
+ * @description Converts validated link references into JSON-safe metadata entries.
664
+ */
665
+ function linksToJson(links) {
666
+ return links.map((link) => ({
667
+ kind: link.kind,
668
+ url: link.url,
669
+ ...(link.label ? { label: link.label } : {}),
670
+ ...(link.sha256 ? { sha256: link.sha256 } : {}),
671
+ }));
672
+ }
673
+ /**
674
+ * @name readTags
675
+ * @description Reads compact lowercase discovery tags for thematic chat rooms.
676
+ */
677
+ function readTags(value) {
678
+ if (!Array.isArray(value)) {
679
+ return [];
680
+ }
681
+ return value
682
+ .filter((item) => typeof item === 'string')
683
+ .map((item) => item.trim().toLowerCase())
684
+ .filter((item) => /^[a-z0-9][a-z0-9:_-]{0,47}$/.test(item))
685
+ .slice(0, 12);
686
+ }
687
+ /**
688
+ * @name readLinkKind
689
+ * @description Reads a supported group message link reference kind.
690
+ */
691
+ function readLinkKind(value) {
692
+ if (value === 'source' || value === 'attachment' || value === 'market' || value === 'execution') {
693
+ return value;
694
+ }
695
+ return 'reference';
696
+ }
697
+ //# sourceMappingURL=chat-tools.js.map