@gtkx/mcp 2.0.0-beta.1 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,16 @@ import * as fs from "node:fs";
3
3
  import * as net from "node:net";
4
4
  import { basename, dirname, join, resolve as resolvePath } from "node:path";
5
5
  import type { ConnectionRegistry } from "./connection-registry.js";
6
- import { DEFAULT_SOCKET_PATH } from "./protocol/schemas.js";
6
+ import {
7
+ cleanupMcpSocketAddress,
8
+ type McpSocketAddress,
9
+ prepareMcpSocketAddress,
10
+ resolveMcpSocketAddress,
11
+ } from "./socket-path.js";
7
12
  import { connectionErrorEvent } from "./transport.js";
8
13
 
9
14
  type ProbeOutcome = { kind: "live" } | { kind: "unknown"; code: string } | { kind: "vacant" };
10
- type PathVerdict = ProbeOutcome | { kind: "directory" };
15
+ type PathVerdict = ProbeOutcome | { kind: "invalid" };
11
16
  type ClaimOutcome = "occupied" | "published";
12
17
 
13
18
  const PROBE_TIMEOUT_MS = 1000;
@@ -75,12 +80,6 @@ const acquireClaimLock = async (socketPath: string): Promise<net.Server | null>
75
80
  return lock;
76
81
  };
77
82
 
78
- const releaseClaimLock = async (lock: net.Server | null): Promise<void> => {
79
- if (lock) {
80
- await closeServer(lock);
81
- }
82
- };
83
-
84
83
  const withClaimLock = async <T>(socketPath: string, action: () => Promise<T> | T): Promise<T> => {
85
84
  const lock = await acquireClaimLock(socketPath);
86
85
 
@@ -159,10 +158,10 @@ const undecidedOwnerError = (socketPath: string, code: string): Error =>
159
158
  "Retry, or delete the file by hand once no server is running.",
160
159
  );
161
160
 
162
- const directoryPathError = (socketPath: string): Error =>
161
+ const invalidPathError = (socketPath: string): Error =>
163
162
  new Error(
164
- `The GTKX MCP socket path ${socketPath} is a directory, not a socket. ` +
165
- "Remove it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.",
163
+ `The GTKX MCP socket path ${socketPath} exists and is not a socket. ` +
164
+ "Move it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.",
166
165
  );
167
166
 
168
167
  const listenFailureError = (socketPath: string, code: string): Error =>
@@ -184,8 +183,8 @@ const clearStalePath = async (target: string): Promise<PathVerdict> => {
184
183
  return { kind: "vacant" };
185
184
  }
186
185
 
187
- if (entry.isDirectory()) {
188
- return { kind: "directory" };
186
+ if (!entry.isSocket()) {
187
+ return { kind: "invalid" };
189
188
  }
190
189
 
191
190
  const outcome = await probeUntilConclusive(target);
@@ -204,8 +203,8 @@ const requireVacantPath = async (socketPath: string): Promise<void> => {
204
203
  throw alreadyOwnedError(socketPath);
205
204
  }
206
205
 
207
- if (verdict.kind === "directory") {
208
- throw directoryPathError(socketPath);
206
+ if (verdict.kind === "invalid") {
207
+ throw invalidPathError(socketPath);
209
208
  }
210
209
 
211
210
  if (verdict.kind === "unknown") {
@@ -242,26 +241,18 @@ const publishSocket = async (privatePath: string, socketPath: string): Promise<n
242
241
  throw alreadyOwnedError(socketPath);
243
242
  };
244
243
 
245
- const releaseSocketPath = async (socketPath: string, inode: number): Promise<void> => {
246
- const lock = await acquireClaimLock(socketPath);
247
-
248
- try {
249
- removeEntry(socketPath, inode);
250
- } finally {
251
- await releaseClaimLock(lock);
252
- }
253
- };
254
-
255
244
  class SocketServer {
256
245
  private server: net.Server | null = null;
257
246
  private socketPath: string;
258
247
  private registry: ConnectionRegistry;
248
+ private address: McpSocketAddress;
259
249
  private boundInode: number | null = null;
260
250
  private startup: Promise<void> | null = null;
261
251
 
262
- constructor(registry: ConnectionRegistry, socketPath: string = DEFAULT_SOCKET_PATH) {
252
+ constructor(registry: ConnectionRegistry, address: McpSocketAddress = resolveMcpSocketAddress()) {
263
253
  this.registry = registry;
264
- this.socketPath = socketPath;
254
+ this.address = address;
255
+ this.socketPath = address.path;
265
256
  }
266
257
 
267
258
  private listen(privatePath: string): Promise<net.Server> {
@@ -294,21 +285,46 @@ class SocketServer {
294
285
  }
295
286
 
296
287
  private async bind(): Promise<void> {
288
+ prepareMcpSocketAddress(this.address);
297
289
  const privatePath = privatePathFor(this.socketPath);
298
290
  await clearStalePath(privatePath);
299
291
  const server = await this.listenPrivately(privatePath);
292
+ const privateInode = inodeFor(privatePath);
300
293
 
301
294
  try {
302
- this.boundInode = await publishSocket(privatePath, this.socketPath);
295
+ if (privateInode === null) {
296
+ throw listenFailureError(this.socketPath, "ENOENT");
297
+ }
298
+
299
+ const publishedInode = await publishSocket(privatePath, this.socketPath);
300
+
301
+ if (publishedInode !== privateInode) {
302
+ throw listenFailureError(this.socketPath, "ESTALE");
303
+ }
304
+
305
+ this.boundInode = privateInode;
303
306
  this.server = server;
304
307
  } catch (error) {
305
308
  await closeServer(server);
309
+
310
+ if (privateInode !== null) {
311
+ removeEntry(privatePath, privateInode);
312
+ removeEntry(this.socketPath, privateInode);
313
+ }
314
+
306
315
  throw error;
307
316
  }
308
317
  }
309
318
 
310
319
  private open(): Promise<void> {
311
- return withClaimLock(this.socketPath, () => this.bind());
320
+ return withClaimLock(this.socketPath, async () => {
321
+ try {
322
+ await this.bind();
323
+ } catch (error) {
324
+ cleanupMcpSocketAddress(this.address);
325
+ throw error;
326
+ }
327
+ });
312
328
  }
313
329
 
314
330
  private async settleStartup(): Promise<void> {
@@ -320,13 +336,23 @@ class SocketServer {
320
336
  }
321
337
  }
322
338
 
323
- private async release(): Promise<void> {
339
+ private async release(server: net.Server): Promise<void> {
324
340
  const inode = this.boundInode;
325
- this.boundInode = null;
326
341
 
327
- if (inode !== null) {
328
- await releaseSocketPath(this.socketPath, inode);
329
- }
342
+ await withClaimLock(this.socketPath, async () => {
343
+ this.registry.dispose();
344
+ await closeServer(server);
345
+
346
+ try {
347
+ if (inode !== null) {
348
+ removeEntry(this.socketPath, inode);
349
+ }
350
+ } finally {
351
+ cleanupMcpSocketAddress(this.address);
352
+ }
353
+ });
354
+
355
+ this.boundInode = null;
330
356
  }
331
357
 
332
358
  async start(): Promise<void> {
@@ -352,10 +378,8 @@ class SocketServer {
352
378
  return;
353
379
  }
354
380
 
381
+ await this.release(server);
355
382
  this.server = null;
356
- this.registry.dispose();
357
- await closeServer(server);
358
- await this.release();
359
383
  }
360
384
  }
361
385