sentinelayer-cli 0.19.0 → 0.20.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/package.json +1 -1
- package/src/commands/ai/identity-lifecycle.js +14 -2
- package/src/commands/mcp.js +60 -0
- package/src/commands/session.js +1255 -25
- package/src/legacy-cli.js +16 -11
- package/src/mcp/registry.js +151 -0
- package/src/mcp/session-stdio-server.js +977 -0
- package/src/scan/generator.js +3 -2
- package/src/session/agent-registry.js +118 -0
- package/src/session/checkpoints.js +71 -1
- package/src/session/coordination-guidance.js +3 -2
- package/src/session/listener.js +302 -68
- package/src/session/pricing-ledger.js +34 -4
- package/src/session/recap.js +4 -2
- package/src/session/sync.js +278 -0
- package/src/session/transcript.js +86 -36
- package/src/session/usage.js +5 -5
- package/src/session/wake/claude.js +175 -0
- package/src/session/wake/codex.js +394 -0
- package/src/session/wake/cursor-store.js +69 -0
- package/src/session/wake/dispatcher.js +184 -0
- package/src/session/wake/pump.js +135 -0
- package/src/session/wake/registry.js +80 -0
- package/src/session/wake/resolve-target.js +146 -0
- package/src/session/wake/sentid.js +103 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
|
|
5
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
6
|
+
import { appendToStream } from "../session/stream.js";
|
|
7
|
+
import { eventMatchesAgent } from "../session/listener.js";
|
|
8
|
+
import {
|
|
9
|
+
listSessionMessageActions,
|
|
10
|
+
pollSessionEvents,
|
|
11
|
+
pollSessionEventsBefore,
|
|
12
|
+
syncSessionEventToApi,
|
|
13
|
+
} from "../session/sync.js";
|
|
14
|
+
|
|
15
|
+
export const SESSION_MCP_SERVER_NAME = "sentinelayer-session-mcp";
|
|
16
|
+
export const SESSION_MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
17
|
+
|
|
18
|
+
const MAX_MESSAGE_CHARS = 16_000;
|
|
19
|
+
const MAX_TOOL_LIMIT = 200;
|
|
20
|
+
const DEFAULT_TOOL_LIMIT = 50;
|
|
21
|
+
const SESSION_MCP_CONFIRM_ATTEMPTS = 3;
|
|
22
|
+
const SESSION_MCP_CONFIRM_DELAY_MS = 250;
|
|
23
|
+
const SESSION_MCP_CONFIRM_PAGE_LIMIT = 200;
|
|
24
|
+
const SESSION_MCP_CONFIRM_MAX_PAGES = 10;
|
|
25
|
+
const JSON_RPC_VERSION = "2.0";
|
|
26
|
+
const CONTENT_LENGTH_PREFIX = "content-length:";
|
|
27
|
+
|
|
28
|
+
function normalizeString(value) {
|
|
29
|
+
return String(value == null ? "" : value).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeAgentId(value, fallbackValue = "") {
|
|
33
|
+
const normalized = normalizeString(value)
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
36
|
+
.replace(/^-+|-+$/g, "");
|
|
37
|
+
return normalized || fallbackValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
41
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
42
|
+
return fallbackValue;
|
|
43
|
+
}
|
|
44
|
+
const normalized = Number(value);
|
|
45
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
46
|
+
return fallbackValue;
|
|
47
|
+
}
|
|
48
|
+
return Math.floor(normalized);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeLimit(value) {
|
|
52
|
+
return Math.max(1, Math.min(MAX_TOOL_LIMIT, normalizePositiveInteger(value, DEFAULT_TOOL_LIMIT)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function truncateText(value, maxChars = MAX_MESSAGE_CHARS) {
|
|
56
|
+
const normalized = normalizeString(value);
|
|
57
|
+
if (normalized.length <= maxChars) {
|
|
58
|
+
return { text: normalized, truncated: false };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
text: normalized.slice(0, maxChars),
|
|
62
|
+
truncated: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRecipients(value) {
|
|
67
|
+
if (value === undefined || value === null) return [];
|
|
68
|
+
const items = Array.isArray(value) ? value : String(value).split(/[\s,;]+/g);
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const recipients = [];
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const normalized = normalizeAgentId(item, "");
|
|
73
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
74
|
+
seen.add(normalized);
|
|
75
|
+
recipients.push(normalized);
|
|
76
|
+
}
|
|
77
|
+
return recipients;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPlainObject(value) {
|
|
81
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cleanObject(record = {}) {
|
|
85
|
+
const cleaned = {};
|
|
86
|
+
for (const [key, value] of Object.entries(record)) {
|
|
87
|
+
if (value !== undefined) {
|
|
88
|
+
cleaned[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return cleaned;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function eventAgentId(event = {}) {
|
|
95
|
+
return normalizeAgentId(event?.agent?.id || event?.agentId || event?.agent_id, "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function eventName(event = {}) {
|
|
99
|
+
return normalizeString(event?.event || event?.type).toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isControlEvent(event = {}) {
|
|
103
|
+
const name = eventName(event);
|
|
104
|
+
const payload = isPlainObject(event?.payload) ? event.payload : {};
|
|
105
|
+
return (
|
|
106
|
+
name.startsWith("session_listen_") ||
|
|
107
|
+
name === "agent_heartbeat" ||
|
|
108
|
+
normalizeString(payload.source).toLowerCase() === "session_listen"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function safePayload(payload = {}) {
|
|
113
|
+
if (!isPlainObject(payload)) return {};
|
|
114
|
+
const next = { ...payload };
|
|
115
|
+
if (typeof next.message === "string") {
|
|
116
|
+
const truncated = truncateText(next.message, MAX_MESSAGE_CHARS);
|
|
117
|
+
next.message = truncated.text;
|
|
118
|
+
if (truncated.truncated) next.truncated = true;
|
|
119
|
+
}
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summarizeSessionEvent(event = {}) {
|
|
124
|
+
return cleanObject({
|
|
125
|
+
cursor: normalizeString(event.cursor) || null,
|
|
126
|
+
sequenceId: normalizePositiveInteger(event.sequenceId ?? event.sequence_id, null),
|
|
127
|
+
event: normalizeString(event.event || event.type),
|
|
128
|
+
ts: normalizeString(event.ts || event.timestamp || event.createdAt || event.created_at),
|
|
129
|
+
sessionId: normalizeString(event.sessionId || event.session_id),
|
|
130
|
+
agent: isPlainObject(event.agent)
|
|
131
|
+
? cleanObject({
|
|
132
|
+
id: normalizeString(event.agent.id),
|
|
133
|
+
model: normalizeString(event.agent.model),
|
|
134
|
+
role: normalizeString(event.agent.role),
|
|
135
|
+
displayName: normalizeString(event.agent.displayName),
|
|
136
|
+
clientKind: normalizeString(event.agent.clientKind),
|
|
137
|
+
})
|
|
138
|
+
: undefined,
|
|
139
|
+
payload: safePayload(event.payload),
|
|
140
|
+
eventId: normalizeString(event.eventId),
|
|
141
|
+
idempotencyToken: normalizeString(event.idempotencyToken),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sessionEventMatchesClientMessageId(event, clientMessageId) {
|
|
146
|
+
const normalizedClientMessageId = normalizeString(clientMessageId);
|
|
147
|
+
if (!normalizedClientMessageId || !isPlainObject(event)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
151
|
+
const candidates = [
|
|
152
|
+
event.id,
|
|
153
|
+
event.eventId,
|
|
154
|
+
event.event_id,
|
|
155
|
+
event.idempotencyToken,
|
|
156
|
+
event.idempotency_token,
|
|
157
|
+
event.clientMessageId,
|
|
158
|
+
event.client_message_id,
|
|
159
|
+
payload.id,
|
|
160
|
+
payload.messageId,
|
|
161
|
+
payload.message_id,
|
|
162
|
+
payload.eventId,
|
|
163
|
+
payload.event_id,
|
|
164
|
+
payload.idempotencyToken,
|
|
165
|
+
payload.idempotency_token,
|
|
166
|
+
payload.clientMessageId,
|
|
167
|
+
payload.client_message_id,
|
|
168
|
+
];
|
|
169
|
+
return candidates.some((candidate) => normalizeString(candidate) === normalizedClientMessageId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function readSessionConfirmationAnchor(sessionId, { targetPath, pollSessionEventsBeforeFn = pollSessionEventsBefore } = {}) {
|
|
173
|
+
const result = await pollSessionEventsBeforeFn(sessionId, {
|
|
174
|
+
targetPath,
|
|
175
|
+
limit: 1,
|
|
176
|
+
forceCircuitProbe: true,
|
|
177
|
+
});
|
|
178
|
+
if (!result?.ok) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
reason: normalizeString(result?.reason) || "confirmation_anchor_failed",
|
|
182
|
+
cursor: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
186
|
+
const lastEvent = events[events.length - 1] || null;
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
reason: "",
|
|
190
|
+
cursor: normalizeString(lastEvent?.cursor) || normalizeString(result.cursor) || null,
|
|
191
|
+
sequenceId: normalizePositiveInteger(lastEvent?.sequenceId ?? lastEvent?.sequence_id, null),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function confirmSessionEventVisible(
|
|
196
|
+
sessionId,
|
|
197
|
+
clientMessageId,
|
|
198
|
+
{
|
|
199
|
+
targetPath,
|
|
200
|
+
anchorCursor = null,
|
|
201
|
+
pollSessionEventsFn = pollSessionEvents,
|
|
202
|
+
sleepFn = sleep,
|
|
203
|
+
attempts = SESSION_MCP_CONFIRM_ATTEMPTS,
|
|
204
|
+
delayMs = SESSION_MCP_CONFIRM_DELAY_MS,
|
|
205
|
+
pageLimit = SESSION_MCP_CONFIRM_PAGE_LIMIT,
|
|
206
|
+
maxPages = SESSION_MCP_CONFIRM_MAX_PAGES,
|
|
207
|
+
} = {},
|
|
208
|
+
) {
|
|
209
|
+
let lastReason = "not_visible";
|
|
210
|
+
let checked = 0;
|
|
211
|
+
let pages = 0;
|
|
212
|
+
const normalizedAnchorCursor = normalizeString(anchorCursor) || null;
|
|
213
|
+
const normalizedAttempts = normalizePositiveInteger(attempts, SESSION_MCP_CONFIRM_ATTEMPTS);
|
|
214
|
+
const normalizedDelayMs = normalizePositiveInteger(delayMs, SESSION_MCP_CONFIRM_DELAY_MS);
|
|
215
|
+
const normalizedPageLimit = normalizeLimit(pageLimit);
|
|
216
|
+
const normalizedMaxPages = normalizePositiveInteger(maxPages, SESSION_MCP_CONFIRM_MAX_PAGES);
|
|
217
|
+
|
|
218
|
+
for (let attempt = 1; attempt <= normalizedAttempts; attempt += 1) {
|
|
219
|
+
let pageCursor = normalizedAnchorCursor;
|
|
220
|
+
for (let page = 1; page <= normalizedMaxPages; page += 1) {
|
|
221
|
+
pages += 1;
|
|
222
|
+
const result = await pollSessionEventsFn(sessionId, {
|
|
223
|
+
targetPath,
|
|
224
|
+
since: pageCursor,
|
|
225
|
+
limit: normalizedPageLimit,
|
|
226
|
+
forceCircuitProbe: true,
|
|
227
|
+
});
|
|
228
|
+
if (!result?.ok) {
|
|
229
|
+
lastReason = normalizeString(result?.reason) || "confirmation_poll_failed";
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
233
|
+
checked += events.length;
|
|
234
|
+
const confirmedEvent = events.find((candidate) => sessionEventMatchesClientMessageId(candidate, clientMessageId));
|
|
235
|
+
if (confirmedEvent) {
|
|
236
|
+
return {
|
|
237
|
+
confirmed: true,
|
|
238
|
+
reason: "",
|
|
239
|
+
checked,
|
|
240
|
+
pages,
|
|
241
|
+
anchorCursor: normalizedAnchorCursor,
|
|
242
|
+
event: confirmedEvent,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
lastReason = "not_visible";
|
|
246
|
+
const nextCursor = normalizeString(result.cursor);
|
|
247
|
+
if (!events.length || !nextCursor || nextCursor === pageCursor) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
pageCursor = nextCursor;
|
|
251
|
+
}
|
|
252
|
+
if (attempt < normalizedAttempts) {
|
|
253
|
+
await sleepFn(normalizedDelayMs);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
confirmed: false,
|
|
259
|
+
reason: lastReason,
|
|
260
|
+
checked,
|
|
261
|
+
pages,
|
|
262
|
+
anchorCursor: normalizedAnchorCursor,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function messageActionActorId(action = {}) {
|
|
267
|
+
return normalizeAgentId(action.actorId || action.actor_id || action.agentId || action.agent_id, "unknown");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function messageActionCreatedMs(action = {}) {
|
|
271
|
+
const epoch = Date.parse(normalizeString(action.createdAt || action.created_at || action.ts || action.timestamp));
|
|
272
|
+
return Number.isFinite(epoch) ? epoch : 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isHumanMessageAction(action = {}) {
|
|
276
|
+
if (action?.isHumanActivity === true) return true;
|
|
277
|
+
if (normalizeString(action.actorKind || action.actor_kind).toLowerCase() === "human") return true;
|
|
278
|
+
return messageActionActorId(action).startsWith("human-");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function summarizeMessageActionActivity(action = {}) {
|
|
282
|
+
return cleanObject({
|
|
283
|
+
id: normalizeString(action.id),
|
|
284
|
+
sessionId: normalizeString(action.sessionId || action.session_id),
|
|
285
|
+
targetSequenceId: normalizePositiveInteger(action.targetSequenceId ?? action.target_sequence_id, null),
|
|
286
|
+
targetCursor: normalizeString(action.targetCursor || action.target_cursor),
|
|
287
|
+
targetActionId: normalizeString(action.targetActionId || action.target_action_id),
|
|
288
|
+
actionType: normalizeString(action.actionType || action.action_type),
|
|
289
|
+
actorKind: normalizeString(action.actorKind || action.actor_kind),
|
|
290
|
+
actorId: normalizeString(action.actorId || action.actor_id),
|
|
291
|
+
actorRole: normalizeString(action.actorRole || action.actor_role),
|
|
292
|
+
note: truncateText(action.note || action.message || "").text,
|
|
293
|
+
createdAt: normalizeString(action.createdAt || action.created_at || action.ts || action.timestamp),
|
|
294
|
+
activityType: normalizeString(action.activityType || action.activity_type) || "message_action",
|
|
295
|
+
isHumanActivity: isHumanMessageAction(action),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function recentHumanActivityFromActions(remoteActions = null, { limit = DEFAULT_TOOL_LIMIT } = {}) {
|
|
300
|
+
const projected = remoteActions?.projection?.recentActivity;
|
|
301
|
+
const source = Array.isArray(projected) && projected.length > 0 ? projected : remoteActions?.actions;
|
|
302
|
+
return Array.isArray(source)
|
|
303
|
+
? source
|
|
304
|
+
.filter((action) => action && typeof action === "object" && isHumanMessageAction(action))
|
|
305
|
+
.sort((left, right) => {
|
|
306
|
+
const timeDiff = messageActionCreatedMs(right) - messageActionCreatedMs(left);
|
|
307
|
+
if (timeDiff !== 0) return timeDiff;
|
|
308
|
+
return normalizeString(right.id).localeCompare(normalizeString(left.id));
|
|
309
|
+
})
|
|
310
|
+
.slice(0, normalizeLimit(limit))
|
|
311
|
+
.map((action) => summarizeMessageActionActivity(action))
|
|
312
|
+
: [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function requireSessionId(input = {}) {
|
|
316
|
+
const sessionId = normalizeString(input.sessionId || input.session_id || input.session);
|
|
317
|
+
if (!sessionId) {
|
|
318
|
+
throw new Error("sessionId is required.");
|
|
319
|
+
}
|
|
320
|
+
return sessionId;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function requireAgentId(input = {}) {
|
|
324
|
+
const agentId = normalizeAgentId(input.agentId || input.agent_id || input.agent, "");
|
|
325
|
+
if (!agentId || agentId === "cli-user" || agentId === "unknown" || agentId.startsWith("human-")) {
|
|
326
|
+
throw new Error("agentId must be a non-human agent id.");
|
|
327
|
+
}
|
|
328
|
+
return agentId;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildAgentEnvelope(agentId, input = {}) {
|
|
332
|
+
return cleanObject({
|
|
333
|
+
id: agentId,
|
|
334
|
+
model: normalizeString(input.model || input.agentModel || input.agent_model) || "mcp",
|
|
335
|
+
role: normalizeString(input.role) || "coder",
|
|
336
|
+
displayName: normalizeString(input.displayName || input.display_name),
|
|
337
|
+
clientKind: "mcp",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildSessionMessageEvent({
|
|
342
|
+
event,
|
|
343
|
+
sessionId,
|
|
344
|
+
agent,
|
|
345
|
+
message,
|
|
346
|
+
recipients = [],
|
|
347
|
+
priority = "",
|
|
348
|
+
idempotencyKey = "",
|
|
349
|
+
nowIso = new Date().toISOString(),
|
|
350
|
+
uuid = randomUUID,
|
|
351
|
+
extraPayload = {},
|
|
352
|
+
} = {}) {
|
|
353
|
+
const messageShape = truncateText(message);
|
|
354
|
+
if (!messageShape.text) {
|
|
355
|
+
throw new Error("message is required.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const eventId = normalizeString(idempotencyKey) || `mcp-${event}-${uuid()}`;
|
|
359
|
+
const payload = cleanObject({
|
|
360
|
+
message: messageShape.text,
|
|
361
|
+
channel: "session",
|
|
362
|
+
source: "mcp",
|
|
363
|
+
clientKind: "mcp",
|
|
364
|
+
to: recipients.length > 0 ? recipients : undefined,
|
|
365
|
+
priority: normalizeString(priority) || undefined,
|
|
366
|
+
truncated: messageShape.truncated || undefined,
|
|
367
|
+
...extraPayload,
|
|
368
|
+
clientMessageId: eventId,
|
|
369
|
+
});
|
|
370
|
+
return createAgentEvent({
|
|
371
|
+
event,
|
|
372
|
+
agent,
|
|
373
|
+
sessionId,
|
|
374
|
+
ts: nowIso,
|
|
375
|
+
payload,
|
|
376
|
+
eventId,
|
|
377
|
+
idempotencyToken: eventId,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function persistSessionEvent({
|
|
382
|
+
sessionId,
|
|
383
|
+
event,
|
|
384
|
+
targetPath,
|
|
385
|
+
dryRun = false,
|
|
386
|
+
syncSessionEventToApiFn = syncSessionEventToApi,
|
|
387
|
+
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
388
|
+
pollSessionEventsFn = pollSessionEvents,
|
|
389
|
+
appendToStreamFn = appendToStream,
|
|
390
|
+
sleepFn = sleep,
|
|
391
|
+
confirmationAttempts = SESSION_MCP_CONFIRM_ATTEMPTS,
|
|
392
|
+
confirmationDelayMs = SESSION_MCP_CONFIRM_DELAY_MS,
|
|
393
|
+
} = {}) {
|
|
394
|
+
if (dryRun) {
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
dryRun: true,
|
|
398
|
+
remoteSync: { synced: false, reason: "dry_run" },
|
|
399
|
+
localCache: { cached: false, reason: "dry_run" },
|
|
400
|
+
event: summarizeSessionEvent(event),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const clientMessageId = normalizeString(event?.payload?.clientMessageId || event?.idempotencyToken || event?.eventId);
|
|
405
|
+
const remoteConfirmationAnchor = await readSessionConfirmationAnchor(sessionId, {
|
|
406
|
+
targetPath,
|
|
407
|
+
pollSessionEventsBeforeFn,
|
|
408
|
+
});
|
|
409
|
+
if (!remoteConfirmationAnchor?.ok) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: remoteConfirmationAnchor?.reason || "confirmation_anchor_failed",
|
|
413
|
+
remoteSync: { synced: false, reason: "not_attempted" },
|
|
414
|
+
remoteConfirmationAnchor,
|
|
415
|
+
remoteConfirmation: {
|
|
416
|
+
confirmed: false,
|
|
417
|
+
reason: remoteConfirmationAnchor?.reason || "confirmation_anchor_failed",
|
|
418
|
+
checked: 0,
|
|
419
|
+
pages: 0,
|
|
420
|
+
anchorCursor: null,
|
|
421
|
+
},
|
|
422
|
+
localCache: { cached: false, reason: "confirmation_anchor_failed" },
|
|
423
|
+
event: summarizeSessionEvent(event),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const remoteSync = await syncSessionEventToApiFn(sessionId, event, { targetPath });
|
|
428
|
+
if (!remoteSync?.synced) {
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
reason: remoteSync?.reason || "remote_sync_failed",
|
|
432
|
+
remoteSync,
|
|
433
|
+
remoteConfirmationAnchor,
|
|
434
|
+
remoteConfirmation: { confirmed: false, reason: "remote_sync_failed", checked: 0, pages: 0, anchorCursor: remoteConfirmationAnchor.cursor },
|
|
435
|
+
localCache: { cached: false, reason: "remote_sync_failed" },
|
|
436
|
+
event: summarizeSessionEvent(event),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const remoteConfirmation = await confirmSessionEventVisible(sessionId, clientMessageId, {
|
|
441
|
+
targetPath,
|
|
442
|
+
anchorCursor: remoteConfirmationAnchor.cursor,
|
|
443
|
+
pollSessionEventsFn,
|
|
444
|
+
sleepFn,
|
|
445
|
+
attempts: confirmationAttempts,
|
|
446
|
+
delayMs: confirmationDelayMs,
|
|
447
|
+
});
|
|
448
|
+
if (!remoteConfirmation?.confirmed) {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
reason: remoteConfirmation?.reason || "remote_not_visible",
|
|
452
|
+
remoteSync,
|
|
453
|
+
remoteConfirmationAnchor,
|
|
454
|
+
remoteConfirmation,
|
|
455
|
+
localCache: { cached: false, reason: "remote_not_visible" },
|
|
456
|
+
event: summarizeSessionEvent(event),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let localCache = { cached: false, reason: "local_session_unavailable" };
|
|
461
|
+
try {
|
|
462
|
+
const persisted = await appendToStreamFn(sessionId, event, {
|
|
463
|
+
targetPath,
|
|
464
|
+
syncRemote: false,
|
|
465
|
+
});
|
|
466
|
+
localCache = {
|
|
467
|
+
cached: true,
|
|
468
|
+
event: summarizeSessionEvent(persisted),
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
localCache = {
|
|
472
|
+
cached: false,
|
|
473
|
+
reason: normalizeString(error?.message) || "local_cache_failed",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
reason: "",
|
|
480
|
+
remoteSync,
|
|
481
|
+
remoteConfirmationAnchor,
|
|
482
|
+
remoteConfirmation,
|
|
483
|
+
localCache,
|
|
484
|
+
event: summarizeSessionEvent(event),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function createSessionMcpToolHandlers({
|
|
489
|
+
targetPath = process.cwd(),
|
|
490
|
+
pollSessionEventsFn = pollSessionEvents,
|
|
491
|
+
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
492
|
+
listSessionMessageActionsFn = listSessionMessageActions,
|
|
493
|
+
syncSessionEventToApiFn = syncSessionEventToApi,
|
|
494
|
+
appendToStreamFn = appendToStream,
|
|
495
|
+
sleepFn = sleep,
|
|
496
|
+
uuidFn = randomUUID,
|
|
497
|
+
now = () => new Date().toISOString(),
|
|
498
|
+
} = {}) {
|
|
499
|
+
return {
|
|
500
|
+
async poll_inbox(input = {}) {
|
|
501
|
+
const sessionId = requireSessionId(input);
|
|
502
|
+
const agentId = requireAgentId(input);
|
|
503
|
+
const cursor = normalizeString(input.cursor || input.after || input.since) || null;
|
|
504
|
+
const limit = normalizeLimit(input.limit);
|
|
505
|
+
const actionLimit = normalizeLimit(input.actionLimit || input.action_limit || limit);
|
|
506
|
+
const includeActions = input.includeActions !== false && input.include_actions !== false;
|
|
507
|
+
const includeSelf = Boolean(input.includeSelf || input.include_self);
|
|
508
|
+
const includeControlEvents = Boolean(input.includeControlEvents || input.include_control_events);
|
|
509
|
+
const includeRaw = Boolean(input.includeRaw || input.include_raw);
|
|
510
|
+
|
|
511
|
+
const result = await pollSessionEventsFn(sessionId, {
|
|
512
|
+
targetPath,
|
|
513
|
+
since: cursor,
|
|
514
|
+
limit,
|
|
515
|
+
forceCircuitProbe: Boolean(input.forceCircuitProbe || input.force_circuit_probe),
|
|
516
|
+
});
|
|
517
|
+
if (!result?.ok) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
reason: result?.reason || "poll_failed",
|
|
521
|
+
sessionId,
|
|
522
|
+
agentId,
|
|
523
|
+
cursor,
|
|
524
|
+
events: [],
|
|
525
|
+
eventCount: 0,
|
|
526
|
+
inboxCount: 0,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const events = (Array.isArray(result.events) ? result.events : []).filter((event) => {
|
|
531
|
+
if (!includeControlEvents && isControlEvent(event)) return false;
|
|
532
|
+
if (!includeSelf && eventAgentId(event) === agentId) return false;
|
|
533
|
+
return eventMatchesAgent(event, agentId);
|
|
534
|
+
});
|
|
535
|
+
const actionResult = includeActions
|
|
536
|
+
? await listSessionMessageActionsFn(sessionId, {
|
|
537
|
+
targetPath,
|
|
538
|
+
limit: actionLimit,
|
|
539
|
+
forceCircuitProbe: Boolean(input.forceCircuitProbe || input.force_circuit_probe),
|
|
540
|
+
})
|
|
541
|
+
: null;
|
|
542
|
+
const recentHumanActivity = actionResult?.ok
|
|
543
|
+
? recentHumanActivityFromActions(actionResult, { limit: actionLimit })
|
|
544
|
+
: [];
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
reason: "",
|
|
549
|
+
sessionId,
|
|
550
|
+
agentId,
|
|
551
|
+
cursor: normalizeString(result.cursor) || cursor,
|
|
552
|
+
eventCount: Array.isArray(result.events) ? result.events.length : 0,
|
|
553
|
+
inboxCount: events.length,
|
|
554
|
+
recentHumanActivityCount: recentHumanActivity.length,
|
|
555
|
+
recentHumanActivity,
|
|
556
|
+
actionProjection: actionResult
|
|
557
|
+
? {
|
|
558
|
+
ok: Boolean(actionResult.ok),
|
|
559
|
+
reason: actionResult.reason || "",
|
|
560
|
+
count: Array.isArray(actionResult.actions) ? actionResult.actions.length : 0,
|
|
561
|
+
}
|
|
562
|
+
: null,
|
|
563
|
+
events: includeRaw ? events : events.map((event) => summarizeSessionEvent(event)),
|
|
564
|
+
};
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
async send_message(input = {}) {
|
|
568
|
+
const sessionId = requireSessionId(input);
|
|
569
|
+
const agentId = requireAgentId(input);
|
|
570
|
+
const recipients = normalizeRecipients(input.to || input.recipient || input.recipients);
|
|
571
|
+
const agent = buildAgentEnvelope(agentId, input);
|
|
572
|
+
const event = buildSessionMessageEvent({
|
|
573
|
+
event: "session_message",
|
|
574
|
+
sessionId,
|
|
575
|
+
agent,
|
|
576
|
+
message: input.message || input.text,
|
|
577
|
+
recipients,
|
|
578
|
+
idempotencyKey: input.idempotencyKey || input.idempotency_key,
|
|
579
|
+
nowIso: now(),
|
|
580
|
+
uuid: uuidFn,
|
|
581
|
+
});
|
|
582
|
+
return persistSessionEvent({
|
|
583
|
+
sessionId,
|
|
584
|
+
event,
|
|
585
|
+
targetPath,
|
|
586
|
+
dryRun: Boolean(input.dryRun || input.dry_run),
|
|
587
|
+
syncSessionEventToApiFn,
|
|
588
|
+
pollSessionEventsBeforeFn,
|
|
589
|
+
pollSessionEventsFn,
|
|
590
|
+
appendToStreamFn,
|
|
591
|
+
sleepFn,
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
async attention_request(input = {}) {
|
|
596
|
+
const sessionId = requireSessionId(input);
|
|
597
|
+
const agentId = requireAgentId(input);
|
|
598
|
+
const recipients = normalizeRecipients(input.to || input.recipient || input.recipients);
|
|
599
|
+
const agent = buildAgentEnvelope(agentId, {
|
|
600
|
+
...input,
|
|
601
|
+
role: normalizeString(input.role) || "observer",
|
|
602
|
+
});
|
|
603
|
+
const event = buildSessionMessageEvent({
|
|
604
|
+
event: "help_request",
|
|
605
|
+
sessionId,
|
|
606
|
+
agent,
|
|
607
|
+
message: input.message || input.reason || input.text,
|
|
608
|
+
recipients,
|
|
609
|
+
priority: normalizeString(input.priority) || "high",
|
|
610
|
+
idempotencyKey: input.idempotencyKey || input.idempotency_key,
|
|
611
|
+
nowIso: now(),
|
|
612
|
+
uuid: uuidFn,
|
|
613
|
+
extraPayload: cleanObject({
|
|
614
|
+
requestType: "attention",
|
|
615
|
+
severity: normalizeString(input.severity) || "normal",
|
|
616
|
+
}),
|
|
617
|
+
});
|
|
618
|
+
return persistSessionEvent({
|
|
619
|
+
sessionId,
|
|
620
|
+
event,
|
|
621
|
+
targetPath,
|
|
622
|
+
dryRun: Boolean(input.dryRun || input.dry_run),
|
|
623
|
+
syncSessionEventToApiFn,
|
|
624
|
+
pollSessionEventsBeforeFn,
|
|
625
|
+
pollSessionEventsFn,
|
|
626
|
+
appendToStreamFn,
|
|
627
|
+
sleepFn,
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export const SESSION_MCP_TOOLS = Object.freeze([
|
|
634
|
+
{
|
|
635
|
+
name: "poll_inbox",
|
|
636
|
+
title: "Poll Senti Inbox",
|
|
637
|
+
description:
|
|
638
|
+
"Poll durable SentinelLayer session events after an optional cursor and return only events addressed or visible to the agent.",
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: "object",
|
|
641
|
+
additionalProperties: false,
|
|
642
|
+
required: ["sessionId", "agentId"],
|
|
643
|
+
properties: {
|
|
644
|
+
sessionId: { type: "string", minLength: 1 },
|
|
645
|
+
agentId: { type: "string", minLength: 1 },
|
|
646
|
+
cursor: { type: "string" },
|
|
647
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_TOOL_LIMIT, default: DEFAULT_TOOL_LIMIT },
|
|
648
|
+
actionLimit: { type: "integer", minimum: 1, maximum: MAX_TOOL_LIMIT, default: DEFAULT_TOOL_LIMIT },
|
|
649
|
+
includeActions: { type: "boolean", default: true },
|
|
650
|
+
includeSelf: { type: "boolean", default: false },
|
|
651
|
+
includeControlEvents: { type: "boolean", default: false },
|
|
652
|
+
includeRaw: { type: "boolean", default: false },
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "send_message",
|
|
658
|
+
title: "Send Senti Message",
|
|
659
|
+
description:
|
|
660
|
+
"Send an authenticated agent session_message through the canonical SentinelLayer session event API.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
additionalProperties: false,
|
|
664
|
+
required: ["sessionId", "agentId", "message"],
|
|
665
|
+
properties: {
|
|
666
|
+
sessionId: { type: "string", minLength: 1 },
|
|
667
|
+
agentId: { type: "string", minLength: 1 },
|
|
668
|
+
message: { type: "string", minLength: 1, maxLength: MAX_MESSAGE_CHARS },
|
|
669
|
+
to: {
|
|
670
|
+
anyOf: [
|
|
671
|
+
{ type: "string" },
|
|
672
|
+
{ type: "array", items: { type: "string" } },
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
model: { type: "string" },
|
|
676
|
+
role: { type: "string" },
|
|
677
|
+
displayName: { type: "string" },
|
|
678
|
+
idempotencyKey: { type: "string" },
|
|
679
|
+
dryRun: { type: "boolean", default: false },
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "attention_request",
|
|
685
|
+
title: "Request Senti Attention",
|
|
686
|
+
description:
|
|
687
|
+
"Create a help_request event for high-signal agent or human attention without relying on chat polling.",
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: "object",
|
|
690
|
+
additionalProperties: false,
|
|
691
|
+
required: ["sessionId", "agentId", "message"],
|
|
692
|
+
properties: {
|
|
693
|
+
sessionId: { type: "string", minLength: 1 },
|
|
694
|
+
agentId: { type: "string", minLength: 1 },
|
|
695
|
+
message: { type: "string", minLength: 1, maxLength: MAX_MESSAGE_CHARS },
|
|
696
|
+
to: {
|
|
697
|
+
anyOf: [
|
|
698
|
+
{ type: "string" },
|
|
699
|
+
{ type: "array", items: { type: "string" } },
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
priority: { type: "string", default: "high" },
|
|
703
|
+
severity: { type: "string", default: "normal" },
|
|
704
|
+
model: { type: "string" },
|
|
705
|
+
role: { type: "string" },
|
|
706
|
+
displayName: { type: "string" },
|
|
707
|
+
idempotencyKey: { type: "string" },
|
|
708
|
+
dryRun: { type: "boolean", default: false },
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
]);
|
|
713
|
+
|
|
714
|
+
function toMcpTool(tool) {
|
|
715
|
+
return {
|
|
716
|
+
name: tool.name,
|
|
717
|
+
title: tool.title,
|
|
718
|
+
description: tool.description,
|
|
719
|
+
inputSchema: tool.inputSchema,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function buildToolResult(payload, { isError = false } = {}) {
|
|
724
|
+
const structuredContent = isPlainObject(payload) ? payload : { value: payload };
|
|
725
|
+
return cleanObject({
|
|
726
|
+
content: [
|
|
727
|
+
{
|
|
728
|
+
type: "text",
|
|
729
|
+
text: JSON.stringify(structuredContent),
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
structuredContent,
|
|
733
|
+
isError: isError ? true : undefined,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function jsonRpcSuccess(id, result) {
|
|
738
|
+
return {
|
|
739
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
740
|
+
id,
|
|
741
|
+
result,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function jsonRpcError(id, code, message, data = undefined) {
|
|
746
|
+
return cleanObject({
|
|
747
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
748
|
+
id: id ?? null,
|
|
749
|
+
error: cleanObject({
|
|
750
|
+
code,
|
|
751
|
+
message,
|
|
752
|
+
data,
|
|
753
|
+
}),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export async function handleMcpJsonRpcMessage(
|
|
758
|
+
message,
|
|
759
|
+
{
|
|
760
|
+
targetPath = process.cwd(),
|
|
761
|
+
handlers = createSessionMcpToolHandlers({ targetPath }),
|
|
762
|
+
tools = SESSION_MCP_TOOLS,
|
|
763
|
+
} = {}
|
|
764
|
+
) {
|
|
765
|
+
if (!isPlainObject(message)) {
|
|
766
|
+
return jsonRpcError(null, -32600, "Invalid Request");
|
|
767
|
+
}
|
|
768
|
+
const id = message.id;
|
|
769
|
+
const method = normalizeString(message.method);
|
|
770
|
+
const isNotification = id === undefined || id === null;
|
|
771
|
+
|
|
772
|
+
if (!method) {
|
|
773
|
+
return isNotification ? null : jsonRpcError(id, -32600, "Invalid Request");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (method === "notifications/initialized") {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (method === "initialize") {
|
|
781
|
+
if (isNotification) return null;
|
|
782
|
+
return jsonRpcSuccess(id, {
|
|
783
|
+
protocolVersion:
|
|
784
|
+
normalizeString(message.params?.protocolVersion) || SESSION_MCP_PROTOCOL_VERSION,
|
|
785
|
+
capabilities: {
|
|
786
|
+
tools: {
|
|
787
|
+
listChanged: false,
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
serverInfo: {
|
|
791
|
+
name: SESSION_MCP_SERVER_NAME,
|
|
792
|
+
version: "0.20.0",
|
|
793
|
+
},
|
|
794
|
+
instructions:
|
|
795
|
+
"Use poll_inbox with the returned cursor before acting; use send_message for durable session posts and attention_request for help_request events.",
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (method === "ping") {
|
|
800
|
+
return isNotification ? null : jsonRpcSuccess(id, {});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (method === "tools/list") {
|
|
804
|
+
if (isNotification) return null;
|
|
805
|
+
return jsonRpcSuccess(id, {
|
|
806
|
+
tools: tools.map((tool) => toMcpTool(tool)),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (method === "tools/call") {
|
|
811
|
+
if (isNotification) return null;
|
|
812
|
+
const toolName = normalizeString(message.params?.name);
|
|
813
|
+
const args = isPlainObject(message.params?.arguments) ? message.params.arguments : {};
|
|
814
|
+
const handler = handlers[toolName];
|
|
815
|
+
if (typeof handler !== "function") {
|
|
816
|
+
return jsonRpcSuccess(
|
|
817
|
+
id,
|
|
818
|
+
buildToolResult(
|
|
819
|
+
{
|
|
820
|
+
ok: false,
|
|
821
|
+
reason: "unknown_tool",
|
|
822
|
+
tool: toolName,
|
|
823
|
+
},
|
|
824
|
+
{ isError: true },
|
|
825
|
+
),
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const result = await handler(args);
|
|
830
|
+
return jsonRpcSuccess(id, buildToolResult(result, { isError: result?.ok === false }));
|
|
831
|
+
} catch (error) {
|
|
832
|
+
return jsonRpcSuccess(
|
|
833
|
+
id,
|
|
834
|
+
buildToolResult(
|
|
835
|
+
{
|
|
836
|
+
ok: false,
|
|
837
|
+
reason: normalizeString(error?.message) || "tool_failed",
|
|
838
|
+
tool: toolName,
|
|
839
|
+
},
|
|
840
|
+
{ isError: true },
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return isNotification ? null : jsonRpcError(id, -32601, "Method not found");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function findHeaderSeparator(buffer) {
|
|
850
|
+
const crlf = buffer.indexOf("\r\n\r\n");
|
|
851
|
+
if (crlf >= 0) {
|
|
852
|
+
return { index: crlf, length: 4 };
|
|
853
|
+
}
|
|
854
|
+
const lf = buffer.indexOf("\n\n");
|
|
855
|
+
if (lf >= 0) {
|
|
856
|
+
return { index: lf, length: 2 };
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function stripLeadingBlankLines(buffer) {
|
|
862
|
+
let start = 0;
|
|
863
|
+
while (start < buffer.length && (buffer[start] === 0x0a || buffer[start] === 0x0d)) {
|
|
864
|
+
start += 1;
|
|
865
|
+
}
|
|
866
|
+
return start > 0 ? buffer.subarray(start) : buffer;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function readNextMcpMessage(buffer) {
|
|
870
|
+
let nextBuffer = stripLeadingBlankLines(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || ""));
|
|
871
|
+
if (nextBuffer.length === 0) return null;
|
|
872
|
+
|
|
873
|
+
const prefix = nextBuffer.subarray(0, Math.min(nextBuffer.length, 32)).toString("utf8").toLowerCase();
|
|
874
|
+
if (prefix.startsWith(CONTENT_LENGTH_PREFIX)) {
|
|
875
|
+
const separator = findHeaderSeparator(nextBuffer);
|
|
876
|
+
if (!separator) return null;
|
|
877
|
+
const header = nextBuffer.subarray(0, separator.index).toString("utf8");
|
|
878
|
+
const lengthLine = header
|
|
879
|
+
.split(/\r?\n/g)
|
|
880
|
+
.find((line) => line.toLowerCase().startsWith(CONTENT_LENGTH_PREFIX));
|
|
881
|
+
const length = Number(normalizeString(lengthLine?.slice(CONTENT_LENGTH_PREFIX.length)));
|
|
882
|
+
if (!Number.isFinite(length) || length < 0) {
|
|
883
|
+
throw new Error("Invalid Content-Length header.");
|
|
884
|
+
}
|
|
885
|
+
const bodyStart = separator.index + separator.length;
|
|
886
|
+
const bodyEnd = bodyStart + length;
|
|
887
|
+
if (nextBuffer.length < bodyEnd) return null;
|
|
888
|
+
return {
|
|
889
|
+
raw: nextBuffer.subarray(bodyStart, bodyEnd).toString("utf8"),
|
|
890
|
+
rest: nextBuffer.subarray(bodyEnd),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const newlineIndex = nextBuffer.indexOf("\n");
|
|
895
|
+
if (newlineIndex < 0) return null;
|
|
896
|
+
const raw = nextBuffer.subarray(0, newlineIndex).toString("utf8").trim();
|
|
897
|
+
return {
|
|
898
|
+
raw,
|
|
899
|
+
rest: nextBuffer.subarray(newlineIndex + 1),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export function writeMcpJsonRpcMessage(stream, message, { framing = "newline" } = {}) {
|
|
904
|
+
const payload = JSON.stringify(message);
|
|
905
|
+
if (normalizeString(framing).toLowerCase() === "content-length") {
|
|
906
|
+
stream.write(`Content-Length: ${Buffer.byteLength(payload, "utf8")}\r\n\r\n${payload}`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
stream.write(`${payload}\n`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export async function runMcpStdioServer({
|
|
913
|
+
stdin = process.stdin,
|
|
914
|
+
stdout = process.stdout,
|
|
915
|
+
stderr = process.stderr,
|
|
916
|
+
targetPath = process.cwd(),
|
|
917
|
+
framing = "newline",
|
|
918
|
+
handlers = createSessionMcpToolHandlers({ targetPath }),
|
|
919
|
+
tools = SESSION_MCP_TOOLS,
|
|
920
|
+
} = {}) {
|
|
921
|
+
let buffer = Buffer.alloc(0);
|
|
922
|
+
let chain = Promise.resolve();
|
|
923
|
+
|
|
924
|
+
function enqueue(raw) {
|
|
925
|
+
chain = chain.then(async () => {
|
|
926
|
+
let message;
|
|
927
|
+
try {
|
|
928
|
+
message = JSON.parse(raw);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
writeMcpJsonRpcMessage(
|
|
931
|
+
stdout,
|
|
932
|
+
jsonRpcError(null, -32700, "Parse error", normalizeString(error?.message)),
|
|
933
|
+
{ framing },
|
|
934
|
+
);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const response = await handleMcpJsonRpcMessage(message, {
|
|
938
|
+
targetPath,
|
|
939
|
+
handlers,
|
|
940
|
+
tools,
|
|
941
|
+
});
|
|
942
|
+
if (response) {
|
|
943
|
+
writeMcpJsonRpcMessage(stdout, response, { framing });
|
|
944
|
+
}
|
|
945
|
+
}).catch((error) => {
|
|
946
|
+
try {
|
|
947
|
+
stderr.write(`${normalizeString(error?.message) || "mcp_server_error"}\n`);
|
|
948
|
+
} catch {
|
|
949
|
+
// Ignore stderr failures; stdout must remain protocol-only.
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return new Promise((resolve, reject) => {
|
|
955
|
+
stdin.on("data", (chunk) => {
|
|
956
|
+
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
957
|
+
try {
|
|
958
|
+
while (true) {
|
|
959
|
+
const parsed = readNextMcpMessage(buffer);
|
|
960
|
+
if (!parsed) break;
|
|
961
|
+
buffer = parsed.rest;
|
|
962
|
+
if (!normalizeString(parsed.raw)) continue;
|
|
963
|
+
enqueue(parsed.raw);
|
|
964
|
+
}
|
|
965
|
+
} catch (error) {
|
|
966
|
+
reject(error);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
stdin.on("error", reject);
|
|
970
|
+
stdin.on("end", () => {
|
|
971
|
+
chain.then(resolve, reject);
|
|
972
|
+
});
|
|
973
|
+
if (typeof stdin.resume === "function") {
|
|
974
|
+
stdin.resume();
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|