@zokizuan/satori-mcp 6.2.0 → 6.3.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.
@@ -0,0 +1,259 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ function readBootId() {
5
+ return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
6
+ }
7
+ export function readLinuxProcessIdentity(pid = process.pid) {
8
+ try {
9
+ const raw = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
10
+ const commandEnd = raw.lastIndexOf(")");
11
+ if (commandEnd < 0)
12
+ return null;
13
+ const fieldsAfterCommand = raw.slice(commandEnd + 2).trim().split(/\s+/);
14
+ const startTime = fieldsAfterCommand[19];
15
+ if (!startTime)
16
+ return null;
17
+ return Object.freeze({
18
+ pid,
19
+ bootId: readBootId(),
20
+ startTime,
21
+ });
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function isLinuxProcessIdentityLive(identity) {
28
+ const current = readLinuxProcessIdentity(identity.pid);
29
+ return current !== null
30
+ && current.bootId === identity.bootId
31
+ && current.startTime === identity.startTime;
32
+ }
33
+ function parseRecord(value) {
34
+ try {
35
+ const parsed = JSON.parse(value);
36
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
37
+ ? parsed
38
+ : null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ export function readHostMetadata(metadataPath) {
45
+ try {
46
+ const record = parseRecord(fs.readFileSync(metadataPath, "utf8"));
47
+ if (!record
48
+ || record.formatVersion !== 1
49
+ || record.protocolVersion !== 1
50
+ || typeof record.hostPid !== "number"
51
+ || typeof record.bootId !== "string"
52
+ || typeof record.processStartTime !== "string"
53
+ || typeof record.mcpVersion !== "string"
54
+ || typeof record.sharedRuntimeIdentityHash !== "string"
55
+ || typeof record.installedRuntimeRoot !== "string"
56
+ || typeof record.ownershipToken !== "string"
57
+ || typeof record.socketPath !== "string"
58
+ || typeof record.socketDevice !== "number"
59
+ || typeof record.socketInode !== "number"
60
+ || typeof record.readyAt !== "string") {
61
+ return null;
62
+ }
63
+ return Object.freeze(record);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ export function writeHostMetadataAtomic(metadataPath, metadata) {
70
+ const temporary = `${metadataPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
71
+ fs.writeFileSync(temporary, `${JSON.stringify(metadata)}\n`, {
72
+ encoding: "utf8",
73
+ mode: 0o600,
74
+ });
75
+ fs.renameSync(temporary, metadataPath);
76
+ }
77
+ function readLockRecord(lockPath) {
78
+ try {
79
+ const record = parseRecord(fs.readFileSync(lockPath, "utf8"));
80
+ if (!record
81
+ || record.formatVersion !== 1
82
+ || typeof record.pid !== "number"
83
+ || typeof record.bootId !== "string"
84
+ || typeof record.processStartTime !== "string"
85
+ || typeof record.ownershipToken !== "string"
86
+ || typeof record.acquiredAt !== "string") {
87
+ return null;
88
+ }
89
+ return Object.freeze(record);
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ function sameFileIdentity(left, right) {
96
+ return left.dev === right.dev && left.ino === right.ino;
97
+ }
98
+ function removeStaleLockWithoutReplacingNewOwner(lockPath) {
99
+ const claimPath = `${lockPath}.${process.pid}.${crypto.randomUUID()}.stale-claim`;
100
+ try {
101
+ fs.linkSync(lockPath, claimPath);
102
+ }
103
+ catch (error) {
104
+ if (error.code === "ENOENT") {
105
+ return true;
106
+ }
107
+ throw error;
108
+ }
109
+ try {
110
+ const claimedOwner = readLockRecord(claimPath);
111
+ if (claimedOwner
112
+ && isLinuxProcessIdentityLive({
113
+ pid: claimedOwner.pid,
114
+ bootId: claimedOwner.bootId,
115
+ startTime: claimedOwner.processStartTime,
116
+ })) {
117
+ return false;
118
+ }
119
+ let lockStat;
120
+ let claimStat;
121
+ try {
122
+ lockStat = fs.lstatSync(lockPath);
123
+ claimStat = fs.lstatSync(claimPath);
124
+ }
125
+ catch (error) {
126
+ if (error.code === "ENOENT") {
127
+ return true;
128
+ }
129
+ throw error;
130
+ }
131
+ if (!sameFileIdentity(lockStat, claimStat) || claimStat.nlink !== 2) {
132
+ return false;
133
+ }
134
+ // Keep our hard-link claim until after unlinking the canonical name.
135
+ // Any concurrent stale cleaner then observes either a different inode,
136
+ // no canonical lock, or more than two links and cannot delete a
137
+ // replacement owner's lock.
138
+ fs.unlinkSync(lockPath);
139
+ return true;
140
+ }
141
+ finally {
142
+ try {
143
+ fs.unlinkSync(claimPath);
144
+ }
145
+ catch {
146
+ // The unique claim is non-authoritative after this attempt.
147
+ }
148
+ }
149
+ }
150
+ function wait(delayMs) {
151
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
152
+ }
153
+ export async function acquireLifecycleLock(lockPath, timeoutMs = 10000) {
154
+ const current = readLinuxProcessIdentity();
155
+ if (!current) {
156
+ throw new Error("Cannot establish Linux process identity for shared runtime lifecycle lock.");
157
+ }
158
+ const ownershipToken = crypto.randomUUID();
159
+ const record = Object.freeze({
160
+ formatVersion: 1,
161
+ pid: current.pid,
162
+ bootId: current.bootId,
163
+ processStartTime: current.startTime,
164
+ ownershipToken,
165
+ acquiredAt: new Date().toISOString(),
166
+ });
167
+ const deadline = Date.now() + timeoutMs;
168
+ for (;;) {
169
+ const candidatePath = `${lockPath}.${current.pid}.${ownershipToken}.candidate`;
170
+ try {
171
+ fs.writeFileSync(candidatePath, `${JSON.stringify(record)}\n`, {
172
+ encoding: "utf8",
173
+ mode: 0o600,
174
+ flag: "wx",
175
+ });
176
+ // A same-filesystem hard link publishes the complete owner record
177
+ // atomically without the empty-file window of open(O_EXCL)+write.
178
+ fs.linkSync(candidatePath, lockPath);
179
+ fs.unlinkSync(candidatePath);
180
+ let released = false;
181
+ return Object.freeze({
182
+ ownershipToken,
183
+ release() {
184
+ if (released)
185
+ return;
186
+ released = true;
187
+ const active = readLockRecord(lockPath);
188
+ if (active?.ownershipToken === ownershipToken) {
189
+ fs.unlinkSync(lockPath);
190
+ }
191
+ },
192
+ });
193
+ }
194
+ catch (error) {
195
+ try {
196
+ fs.unlinkSync(candidatePath);
197
+ }
198
+ catch (cleanupError) {
199
+ if (cleanupError.code !== "ENOENT") {
200
+ throw cleanupError;
201
+ }
202
+ }
203
+ const code = error.code;
204
+ if (code !== "EEXIST")
205
+ throw error;
206
+ }
207
+ const owner = readLockRecord(lockPath);
208
+ const ownerLive = owner
209
+ ? isLinuxProcessIdentityLive({
210
+ pid: owner.pid,
211
+ bootId: owner.bootId,
212
+ startTime: owner.processStartTime,
213
+ })
214
+ : false;
215
+ if (!ownerLive && removeStaleLockWithoutReplacingNewOwner(lockPath)) {
216
+ continue;
217
+ }
218
+ if (Date.now() >= deadline) {
219
+ throw new Error(`Timed out waiting for shared runtime lifecycle lock '${lockPath}'.`);
220
+ }
221
+ await wait(25);
222
+ }
223
+ }
224
+ export function metadataMatchesIdentity(metadata, identity) {
225
+ return metadata.sharedRuntimeIdentityHash === identity.hash
226
+ && metadata.mcpVersion === identity.mcpVersion
227
+ && path.resolve(metadata.installedRuntimeRoot) === path.resolve(identity.installedRuntimeRoot);
228
+ }
229
+ export function removeOwnedLifecycleState(paths, expected) {
230
+ const current = readHostMetadata(paths.metadataPath);
231
+ if (!current
232
+ || current.hostPid !== expected.hostPid
233
+ || current.bootId !== expected.bootId
234
+ || current.processStartTime !== expected.processStartTime
235
+ || current.ownershipToken !== expected.ownershipToken
236
+ || current.sharedRuntimeIdentityHash !== expected.sharedRuntimeIdentityHash
237
+ || current.socketPath !== expected.socketPath
238
+ || current.socketDevice !== expected.socketDevice
239
+ || current.socketInode !== expected.socketInode) {
240
+ return;
241
+ }
242
+ try {
243
+ const socket = fs.lstatSync(expected.socketPath);
244
+ if (socket.isSocket()
245
+ && socket.dev === expected.socketDevice
246
+ && socket.ino === expected.socketInode) {
247
+ fs.unlinkSync(expected.socketPath);
248
+ }
249
+ }
250
+ catch (error) {
251
+ if (error.code !== "ENOENT")
252
+ throw error;
253
+ }
254
+ const stillCurrent = readHostMetadata(paths.metadataPath);
255
+ if (stillCurrent?.ownershipToken === expected.ownershipToken) {
256
+ fs.unlinkSync(paths.metadataPath);
257
+ }
258
+ }
259
+ //# sourceMappingURL=shared-runtime-lifecycle.js.map
@@ -0,0 +1,28 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import type { JSONRPCMessage, MessageExtraInfo } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { Socket } from "node:net";
4
+ export declare class BoundedSocketTransport implements Transport {
5
+ private readonly socket;
6
+ private readonly onSocketClosed;
7
+ private readonly maximumMessageBytes;
8
+ private readonly maximumPendingRequests;
9
+ onclose?: () => void;
10
+ onerror?: (error: Error) => void;
11
+ onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
12
+ private buffer;
13
+ private readonly pendingRequests;
14
+ private started;
15
+ private closed;
16
+ constructor(socket: Socket, onSocketClosed: () => void, initialBytes?: Buffer, maximumMessageBytes?: number, maximumPendingRequests?: number);
17
+ start(): Promise<void>;
18
+ send(message: JSONRPCMessage): Promise<void>;
19
+ close(): Promise<void>;
20
+ private readonly handleData;
21
+ private processBuffer;
22
+ private processCompleteLine;
23
+ private readonly handleError;
24
+ private readonly handleClose;
25
+ private fail;
26
+ private detach;
27
+ }
28
+ //# sourceMappingURL=shared-runtime-transport.d.ts.map
@@ -0,0 +1,158 @@
1
+ import { JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
2
+ import { SHARED_RUNTIME_MAX_PENDING_REQUESTS, SHARED_RUNTIME_MESSAGE_MAX_BYTES, } from "./shared-runtime-identity.js";
3
+ function requestKey(message) {
4
+ if (!("method" in message) || !("id" in message))
5
+ return null;
6
+ return `${typeof message.id}:${String(message.id)}`;
7
+ }
8
+ function responseKey(message) {
9
+ if (!("id" in message)
10
+ || (!("result" in message) && !("error" in message))) {
11
+ return null;
12
+ }
13
+ return `${typeof message.id}:${String(message.id)}`;
14
+ }
15
+ export class BoundedSocketTransport {
16
+ constructor(socket, onSocketClosed, initialBytes, maximumMessageBytes = SHARED_RUNTIME_MESSAGE_MAX_BYTES, maximumPendingRequests = SHARED_RUNTIME_MAX_PENDING_REQUESTS) {
17
+ this.socket = socket;
18
+ this.onSocketClosed = onSocketClosed;
19
+ this.maximumMessageBytes = maximumMessageBytes;
20
+ this.maximumPendingRequests = maximumPendingRequests;
21
+ this.buffer = Buffer.alloc(0);
22
+ this.pendingRequests = new Set();
23
+ this.started = false;
24
+ this.closed = false;
25
+ this.handleData = (chunk) => {
26
+ if (this.closed)
27
+ return;
28
+ let offset = 0;
29
+ while (offset < chunk.length && !this.closed) {
30
+ const newline = chunk.indexOf(0x0a, offset);
31
+ const end = newline < 0 ? chunk.length : newline;
32
+ const segment = chunk.subarray(offset, end);
33
+ if (this.buffer.length + segment.length > this.maximumMessageBytes) {
34
+ this.fail(new Error(`Shared runtime JSON-RPC frame exceeds ${this.maximumMessageBytes} bytes.`));
35
+ return;
36
+ }
37
+ if (segment.length > 0) {
38
+ this.buffer = this.buffer.length === 0
39
+ ? Buffer.from(segment)
40
+ : Buffer.concat([this.buffer, segment]);
41
+ }
42
+ if (newline < 0)
43
+ return;
44
+ this.processCompleteLine();
45
+ offset = newline + 1;
46
+ }
47
+ };
48
+ this.handleError = (error) => {
49
+ this.onerror?.(error);
50
+ };
51
+ this.handleClose = () => {
52
+ if (this.closed)
53
+ return;
54
+ this.closed = true;
55
+ this.detach();
56
+ this.onSocketClosed();
57
+ this.onclose?.();
58
+ };
59
+ if (initialBytes && initialBytes.length > 0) {
60
+ this.buffer = Buffer.from(initialBytes);
61
+ }
62
+ }
63
+ async start() {
64
+ if (this.started) {
65
+ throw new Error("Bounded socket transport is already started.");
66
+ }
67
+ this.started = true;
68
+ this.socket.on("data", this.handleData);
69
+ this.socket.on("error", this.handleError);
70
+ this.socket.on("close", this.handleClose);
71
+ if (this.buffer.length > 0) {
72
+ this.processBuffer();
73
+ }
74
+ this.socket.resume();
75
+ }
76
+ async send(message) {
77
+ if (this.closed || this.socket.destroyed) {
78
+ throw new Error("Shared runtime session transport is closed.");
79
+ }
80
+ const completedRequest = responseKey(message);
81
+ if (completedRequest !== null) {
82
+ this.pendingRequests.delete(completedRequest);
83
+ }
84
+ const serialized = `${JSON.stringify(message)}\n`;
85
+ await new Promise((resolve, reject) => {
86
+ this.socket.write(serialized, (error) => {
87
+ if (error)
88
+ reject(error);
89
+ else
90
+ resolve();
91
+ });
92
+ });
93
+ }
94
+ async close() {
95
+ if (this.closed)
96
+ return;
97
+ this.closed = true;
98
+ this.detach();
99
+ if (!this.socket.destroyed) {
100
+ this.socket.destroy();
101
+ }
102
+ this.onSocketClosed();
103
+ this.onclose?.();
104
+ }
105
+ processBuffer() {
106
+ for (;;) {
107
+ const newline = this.buffer.indexOf(0x0a);
108
+ if (newline < 0) {
109
+ if (this.buffer.length > this.maximumMessageBytes) {
110
+ this.fail(new Error(`Shared runtime JSON-RPC frame exceeds ${this.maximumMessageBytes} bytes.`));
111
+ }
112
+ return;
113
+ }
114
+ const line = this.buffer.subarray(0, newline);
115
+ this.buffer = this.buffer.subarray(newline + 1);
116
+ if (line.length === 0)
117
+ continue;
118
+ this.processCompleteLine(line);
119
+ }
120
+ }
121
+ processCompleteLine(line) {
122
+ const completeLine = line ?? this.buffer;
123
+ if (line === undefined) {
124
+ this.buffer = Buffer.alloc(0);
125
+ }
126
+ const serialized = completeLine.toString("utf8").replace(/\r$/, "");
127
+ if (!serialized)
128
+ return;
129
+ try {
130
+ const message = JSONRPCMessageSchema.parse(JSON.parse(serialized));
131
+ const pendingRequest = requestKey(message);
132
+ if (pendingRequest !== null) {
133
+ if (this.pendingRequests.has(pendingRequest)
134
+ || this.pendingRequests.size >= this.maximumPendingRequests) {
135
+ this.fail(new Error(`Shared runtime session exceeds ${this.maximumPendingRequests} pending requests.`));
136
+ return;
137
+ }
138
+ this.pendingRequests.add(pendingRequest);
139
+ }
140
+ this.onmessage?.(message);
141
+ }
142
+ catch (error) {
143
+ this.fail(error instanceof Error ? error : new Error(String(error)));
144
+ }
145
+ }
146
+ fail(error) {
147
+ this.onerror?.(error);
148
+ void this.close();
149
+ }
150
+ detach() {
151
+ this.socket.off("data", this.handleData);
152
+ this.socket.off("error", this.handleError);
153
+ this.socket.off("close", this.handleClose);
154
+ this.buffer = Buffer.alloc(0);
155
+ this.pendingRequests.clear();
156
+ }
157
+ }
158
+ //# sourceMappingURL=shared-runtime-transport.js.map
@@ -0,0 +1,83 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import type { Readable, Writable } from "node:stream";
3
+ import type { ContextMcpConfig, IndexFingerprint } from "../config.js";
4
+ import { CallGraphSidecarManager } from "../core/call-graph.js";
5
+ import { SearchContinuationCoordinator, ToolHandlers } from "../core/handlers.js";
6
+ import { MutationLeaseCoordinator } from "../core/mutation-lease.js";
7
+ import type { MissingProviderConfigIssue, ProviderBackedOperation, ToolContext } from "../tools/types.js";
8
+ import { ProviderRuntime } from "./provider-runtime.js";
9
+ export type ServerRunMode = "mcp" | "cli" | "postflight" | "host";
10
+ declare class SessionProviderRuntime {
11
+ private readonly providerRuntime;
12
+ private readonly continuationCoordinator;
13
+ private readonly mutationLeaseCoordinator;
14
+ private readonly callGraphManager;
15
+ private readonly contexts;
16
+ private readonly handlers;
17
+ constructor(providerRuntime: ProviderRuntime, continuationCoordinator: SearchContinuationCoordinator, mutationLeaseCoordinator: MutationLeaseCoordinator, callGraphManager: CallGraphSidecarManager);
18
+ requireToolContext(operation: ProviderBackedOperation): Promise<ToolContext | MissingProviderConfigIssue>;
19
+ release(): void;
20
+ }
21
+ type SessionResources = {
22
+ toolContext: ToolContext;
23
+ localHandlers: ToolHandlers;
24
+ providerRuntime: SessionProviderRuntime;
25
+ };
26
+ export declare class SharedRuntimeHost {
27
+ readonly config: ContextMcpConfig;
28
+ readonly runtimeFingerprint: IndexFingerprint;
29
+ readonly runMode: ServerRunMode;
30
+ private readonly capabilities;
31
+ private readonly snapshotManager;
32
+ private readonly callGraphManager;
33
+ private readonly runtimeOwnerRegistry;
34
+ private readonly mutationLeaseCoordinator;
35
+ private readonly localContext;
36
+ private readonly localSyncManager;
37
+ private readonly recoveryHandlers;
38
+ private readonly providerRuntime;
39
+ private readonly readFileMaxLines;
40
+ private readonly watchSyncEnabled;
41
+ private readonly watchDebounceMs;
42
+ private activeSessions;
43
+ private activeOperations;
44
+ private shutdownStarted;
45
+ private readonly activityListeners;
46
+ constructor(config: ContextMcpConfig, runtimeFingerprint: IndexFingerprint, runMode: ServerRunMode);
47
+ createSession(): McpSession;
48
+ createSessionResources(continuationCoordinator: SearchContinuationCoordinator): SessionResources;
49
+ registerSession(): void;
50
+ unregisterSession(): void;
51
+ beginOperation(): void;
52
+ endOperation(): void;
53
+ getActivity(): Readonly<{
54
+ sessions: number;
55
+ operations: number;
56
+ }>;
57
+ getProviderRuntime(): ProviderRuntime;
58
+ subscribeActivity(listener: () => void): () => void;
59
+ private notifyActivityChanged;
60
+ recoverInterruptedIndexingAtStartup(): Promise<void>;
61
+ shutdown(): Promise<void>;
62
+ }
63
+ export declare class McpSession {
64
+ private readonly host;
65
+ private readonly server;
66
+ private readonly continuationCoordinator;
67
+ private readonly resources;
68
+ private activeToolCalls;
69
+ private connected;
70
+ private closed;
71
+ private resourcesReleased;
72
+ private resourceReleasePromise;
73
+ private resolveResourceRelease;
74
+ private keepAliveTimer;
75
+ constructor(host: SharedRuntimeHost);
76
+ private setupTools;
77
+ connect(transport: Transport, resumeInput?: Readable): Promise<void>;
78
+ connectStdio(input?: Readable, output?: Writable): Promise<void>;
79
+ shutdown(): Promise<void>;
80
+ private releaseResourcesIfIdle;
81
+ }
82
+ export {};
83
+ //# sourceMappingURL=shared-runtime.d.ts.map