@gtkx/mcp 2.0.0-beta.1 → 2.0.0-beta.10
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/README.md +43 -40
- package/dist/app-router.d.ts +6 -2
- package/dist/app-router.d.ts.map +1 -1
- package/dist/app-router.js +56 -19
- package/dist/app-router.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/dist/internal.js.map +1 -1
- package/dist/protocol/schemas.d.ts +1 -2
- package/dist/protocol/schemas.d.ts.map +1 -1
- package/dist/protocol/schemas.js +1 -7
- package/dist/protocol/schemas.js.map +1 -1
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +12 -11
- package/dist/reference.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +92 -44
- package/dist/server.js.map +1 -1
- package/dist/socket-path.d.ts +11 -0
- package/dist/socket-path.d.ts.map +1 -0
- package/dist/socket-path.js +74 -0
- package/dist/socket-path.js.map +1 -0
- package/dist/socket-server.d.ts +3 -1
- package/dist/socket-server.d.ts.map +1 -1
- package/dist/socket-server.js +48 -32
- package/dist/socket-server.js.map +1 -1
- package/package.json +9 -7
- package/src/app-router.ts +77 -22
- package/src/internal.ts +4 -1
- package/src/protocol/schemas.ts +0 -9
- package/src/reference.ts +12 -11
- package/src/server.ts +114 -46
- package/src/socket-path.ts +103 -0
- package/src/socket-server.ts +61 -37
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, rmdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const MCP_SOCKET_PATH_ENV = "GTKX_MCP_SOCKET_PATH";
|
|
6
|
+
const SOCKET_NAME = "gtkx-mcp.sock";
|
|
7
|
+
const SOCKET_PATH_BYTE_LIMIT = 100;
|
|
8
|
+
const SHORT_SOCKET_ROOT = "/tmp";
|
|
9
|
+
const socketCandidate = () => process.env[MCP_SOCKET_PATH_ENV] ?? join(process.env.XDG_RUNTIME_DIR ?? tmpdir(), SOCKET_NAME);
|
|
10
|
+
const currentUserId = () => {
|
|
11
|
+
const getuid = process.getuid;
|
|
12
|
+
if (getuid === undefined) {
|
|
13
|
+
throw new Error("GTKX MCP Unix sockets require a platform with user IDs");
|
|
14
|
+
}
|
|
15
|
+
return getuid();
|
|
16
|
+
};
|
|
17
|
+
const verifyPrivateDirectory = (directory, userId) => {
|
|
18
|
+
const entry = lstatSync(directory);
|
|
19
|
+
if (!entry.isDirectory() || entry.uid !== userId || (entry.mode & 0o777) !== 0o700) {
|
|
20
|
+
throw new Error(`GTKX MCP fallback socket directory is not private: ${directory}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const ensurePrivateDirectory = (directory, userId) => {
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(directory, { mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== "EEXIST") {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
verifyPrivateDirectory(directory, userId);
|
|
33
|
+
};
|
|
34
|
+
const fallbackAddress = (candidate) => {
|
|
35
|
+
const userId = currentUserId();
|
|
36
|
+
const digest = createHash("sha256").update(candidate).digest("hex").slice(0, 24);
|
|
37
|
+
const fallbackDirectory = join(SHORT_SOCKET_ROOT, `gtkx-mcp-${String(userId)}-${digest}`);
|
|
38
|
+
try {
|
|
39
|
+
verifyPrivateDirectory(fallbackDirectory, userId);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT") {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { path: join(fallbackDirectory, "socket"), fallbackDirectory };
|
|
47
|
+
};
|
|
48
|
+
const resolveMcpSocketAddress = (providedPath) => {
|
|
49
|
+
const candidate = providedPath ?? socketCandidate();
|
|
50
|
+
return Buffer.byteLength(candidate) <= SOCKET_PATH_BYTE_LIMIT
|
|
51
|
+
? { path: candidate, fallbackDirectory: null }
|
|
52
|
+
: fallbackAddress(candidate);
|
|
53
|
+
};
|
|
54
|
+
const resolveMcpSocketPath = (providedPath) => resolveMcpSocketAddress(providedPath).path;
|
|
55
|
+
const prepareMcpSocketAddress = (address) => {
|
|
56
|
+
if (address.fallbackDirectory !== null) {
|
|
57
|
+
ensurePrivateDirectory(address.fallbackDirectory, currentUserId());
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const cleanupMcpSocketAddress = (address) => {
|
|
61
|
+
const directory = address.fallbackDirectory;
|
|
62
|
+
if (directory === null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
verifyPrivateDirectory(directory, currentUserId());
|
|
67
|
+
rmdirSync(directory);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export { cleanupMcpSocketAddress, MCP_SOCKET_PATH_ENV, prepareMcpSocketAddress, resolveMcpSocketAddress, resolveMcpSocketPath, };
|
|
74
|
+
//# sourceMappingURL=socket-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-path.js","sourceRoot":"","sources":["../src/socket-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOjC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AACnD,MAAM,WAAW,GAAG,eAAe,CAAC;AACpC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEjC,MAAM,eAAe,GAAG,GAAW,EAAE,CACjC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC;AAEnG,MAAM,aAAa,GAAG,GAAW,EAAE;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,MAAM,EAAE,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,SAAiB,EAAE,MAAc,EAAQ,EAAE;IACvE,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IAEnC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC;QACjF,MAAM,IAAI,KAAK,CAAC,sDAAsD,SAAS,EAAE,CAAC,CAAC;IACvF,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,sBAAsB,GAAG,CAAC,SAAiB,EAAE,MAAc,EAAQ,EAAE;IACvE,IAAI,CAAC;QACD,SAAS,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,sBAAsB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,SAAiB,EAAoB,EAAE;IAC5D,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjF,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,EAAE,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;IAE1F,IAAI,CAAC;QACD,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;AAC1E,CAAC,CAAC;AAEF,MAAM,uBAAuB,GAAG,CAAC,YAAqB,EAAoB,EAAE;IACxE,MAAM,SAAS,GAAG,YAAY,IAAI,eAAe,EAAE,CAAC;IAEpD,OAAO,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,sBAAsB;QACzD,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE;QAC9C,CAAC,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CAAC,YAAqB,EAAU,EAAE,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC;AAE3G,MAAM,uBAAuB,GAAG,CAAC,OAAyB,EAAQ,EAAE;IAChE,IAAI,OAAO,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;QACrC,sBAAsB,CAAC,OAAO,CAAC,iBAAiB,EAAE,aAAa,EAAE,CAAC,CAAC;IACvE,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,uBAAuB,GAAG,CAAC,OAAyB,EAAQ,EAAE;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAE5C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO;IACX,CAAC;IAED,IAAI,CAAC;QACD,sBAAsB,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;QACnD,SAAS,CAAC,SAAS,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO;IACX,CAAC;AACL,CAAC,CAAC;AAEF,OAAO,EACH,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,GAEvB,CAAC","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { lstatSync, mkdirSync, rmdirSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\ntype McpSocketAddress = {\n path: string;\n fallbackDirectory: string | null;\n};\n\nconst MCP_SOCKET_PATH_ENV = \"GTKX_MCP_SOCKET_PATH\";\nconst SOCKET_NAME = \"gtkx-mcp.sock\";\nconst SOCKET_PATH_BYTE_LIMIT = 100;\nconst SHORT_SOCKET_ROOT = \"/tmp\";\n\nconst socketCandidate = (): string =>\n process.env[MCP_SOCKET_PATH_ENV] ?? join(process.env.XDG_RUNTIME_DIR ?? tmpdir(), SOCKET_NAME);\n\nconst currentUserId = (): number => {\n const getuid = process.getuid;\n\n if (getuid === undefined) {\n throw new Error(\"GTKX MCP Unix sockets require a platform with user IDs\");\n }\n\n return getuid();\n};\n\nconst verifyPrivateDirectory = (directory: string, userId: number): void => {\n const entry = lstatSync(directory);\n\n if (!entry.isDirectory() || entry.uid !== userId || (entry.mode & 0o777) !== 0o700) {\n throw new Error(`GTKX MCP fallback socket directory is not private: ${directory}`);\n }\n};\n\nconst ensurePrivateDirectory = (directory: string, userId: number): void => {\n try {\n mkdirSync(directory, { mode: 0o700 });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n }\n\n verifyPrivateDirectory(directory, userId);\n};\n\nconst fallbackAddress = (candidate: string): McpSocketAddress => {\n const userId = currentUserId();\n const digest = createHash(\"sha256\").update(candidate).digest(\"hex\").slice(0, 24);\n const fallbackDirectory = join(SHORT_SOCKET_ROOT, `gtkx-mcp-${String(userId)}-${digest}`);\n\n try {\n verifyPrivateDirectory(fallbackDirectory, userId);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error;\n }\n }\n\n return { path: join(fallbackDirectory, \"socket\"), fallbackDirectory };\n};\n\nconst resolveMcpSocketAddress = (providedPath?: string): McpSocketAddress => {\n const candidate = providedPath ?? socketCandidate();\n\n return Buffer.byteLength(candidate) <= SOCKET_PATH_BYTE_LIMIT\n ? { path: candidate, fallbackDirectory: null }\n : fallbackAddress(candidate);\n};\n\nconst resolveMcpSocketPath = (providedPath?: string): string => resolveMcpSocketAddress(providedPath).path;\n\nconst prepareMcpSocketAddress = (address: McpSocketAddress): void => {\n if (address.fallbackDirectory !== null) {\n ensurePrivateDirectory(address.fallbackDirectory, currentUserId());\n }\n};\n\nconst cleanupMcpSocketAddress = (address: McpSocketAddress): void => {\n const directory = address.fallbackDirectory;\n\n if (directory === null) {\n return;\n }\n\n try {\n verifyPrivateDirectory(directory, currentUserId());\n rmdirSync(directory);\n } catch {\n return;\n }\n};\n\nexport {\n cleanupMcpSocketAddress,\n MCP_SOCKET_PATH_ENV,\n prepareMcpSocketAddress,\n resolveMcpSocketAddress,\n resolveMcpSocketPath,\n type McpSocketAddress,\n};\n"]}
|
package/dist/socket-server.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ConnectionRegistry } from "./connection-registry.js";
|
|
2
|
+
import { type McpSocketAddress } from "./socket-path.js";
|
|
2
3
|
declare class SocketServer {
|
|
3
4
|
private server;
|
|
4
5
|
private socketPath;
|
|
5
6
|
private registry;
|
|
7
|
+
private address;
|
|
6
8
|
private boundInode;
|
|
7
9
|
private startup;
|
|
8
|
-
constructor(registry: ConnectionRegistry,
|
|
10
|
+
constructor(registry: ConnectionRegistry, address?: McpSocketAddress);
|
|
9
11
|
private listen;
|
|
10
12
|
private listenPrivately;
|
|
11
13
|
private bind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../src/socket-server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../src/socket-server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAEH,KAAK,gBAAgB,EAGxB,MAAM,kBAAkB,CAAC;AAyO1B,cAAM,YAAY;IACd,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAA8B;gBAEjC,QAAQ,EAAE,kBAAkB,EAAE,OAAO,GAAE,gBAA4C;IAM/F,OAAO,CAAC,MAAM;YAoBA,eAAe;YASf,IAAI;IAgClB,OAAO,CAAC,IAAI;YAWE,aAAa;YASb,OAAO;IAmBf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAetB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAW9B;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/dist/socket-server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
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
|
-
import {
|
|
5
|
+
import { cleanupMcpSocketAddress, prepareMcpSocketAddress, resolveMcpSocketAddress, } from "./socket-path.js";
|
|
6
6
|
import { connectionErrorEvent } from "./transport.js";
|
|
7
7
|
const PROBE_TIMEOUT_MS = 1000;
|
|
8
8
|
const PROBE_ATTEMPTS = 3;
|
|
@@ -49,11 +49,6 @@ const acquireClaimLock = async (socketPath) => {
|
|
|
49
49
|
}
|
|
50
50
|
return lock;
|
|
51
51
|
};
|
|
52
|
-
const releaseClaimLock = async (lock) => {
|
|
53
|
-
if (lock) {
|
|
54
|
-
await closeServer(lock);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
52
|
const withClaimLock = async (socketPath, action) => {
|
|
58
53
|
const lock = await acquireClaimLock(socketPath);
|
|
59
54
|
if (lock === null) {
|
|
@@ -110,8 +105,8 @@ const alreadyOwnedError = (socketPath) => new Error(`Another GTKX MCP server alr
|
|
|
110
105
|
const undecidedOwnerError = (socketPath, code) => new Error(`Could not tell whether another GTKX MCP server owns ${socketPath}: probing it failed with ${code}. ` +
|
|
111
106
|
"Leaving the socket in place instead of removing one that may still be serving another session. " +
|
|
112
107
|
"Retry, or delete the file by hand once no server is running.");
|
|
113
|
-
const
|
|
114
|
-
"
|
|
108
|
+
const invalidPathError = (socketPath) => new Error(`The GTKX MCP socket path ${socketPath} exists and is not a socket. ` +
|
|
109
|
+
"Move it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.");
|
|
115
110
|
const listenFailureError = (socketPath, code) => new Error(`Could not create the GTKX MCP socket at ${socketPath}: listening failed with ${code}. ` +
|
|
116
111
|
"Check that its directory exists and is writable.");
|
|
117
112
|
const removeEntry = (target, inode) => {
|
|
@@ -124,8 +119,8 @@ const clearStalePath = async (target) => {
|
|
|
124
119
|
if (entry === null) {
|
|
125
120
|
return { kind: "vacant" };
|
|
126
121
|
}
|
|
127
|
-
if (entry.
|
|
128
|
-
return { kind: "
|
|
122
|
+
if (!entry.isSocket()) {
|
|
123
|
+
return { kind: "invalid" };
|
|
129
124
|
}
|
|
130
125
|
const outcome = await probeUntilConclusive(target);
|
|
131
126
|
if (outcome.kind === "vacant") {
|
|
@@ -138,8 +133,8 @@ const requireVacantPath = async (socketPath) => {
|
|
|
138
133
|
if (verdict.kind === "live") {
|
|
139
134
|
throw alreadyOwnedError(socketPath);
|
|
140
135
|
}
|
|
141
|
-
if (verdict.kind === "
|
|
142
|
-
throw
|
|
136
|
+
if (verdict.kind === "invalid") {
|
|
137
|
+
throw invalidPathError(socketPath);
|
|
143
138
|
}
|
|
144
139
|
if (verdict.kind === "unknown") {
|
|
145
140
|
throw undecidedOwnerError(socketPath, verdict.code);
|
|
@@ -168,24 +163,17 @@ const publishSocket = async (privatePath, socketPath) => {
|
|
|
168
163
|
}
|
|
169
164
|
throw alreadyOwnedError(socketPath);
|
|
170
165
|
};
|
|
171
|
-
const releaseSocketPath = async (socketPath, inode) => {
|
|
172
|
-
const lock = await acquireClaimLock(socketPath);
|
|
173
|
-
try {
|
|
174
|
-
removeEntry(socketPath, inode);
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
await releaseClaimLock(lock);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
166
|
class SocketServer {
|
|
181
167
|
server = null;
|
|
182
168
|
socketPath;
|
|
183
169
|
registry;
|
|
170
|
+
address;
|
|
184
171
|
boundInode = null;
|
|
185
172
|
startup = null;
|
|
186
|
-
constructor(registry,
|
|
173
|
+
constructor(registry, address = resolveMcpSocketAddress()) {
|
|
187
174
|
this.registry = registry;
|
|
188
|
-
this.
|
|
175
|
+
this.address = address;
|
|
176
|
+
this.socketPath = address.path;
|
|
189
177
|
}
|
|
190
178
|
listen(privatePath) {
|
|
191
179
|
return new Promise((resolve, reject) => {
|
|
@@ -213,20 +201,41 @@ class SocketServer {
|
|
|
213
201
|
}
|
|
214
202
|
}
|
|
215
203
|
async bind() {
|
|
204
|
+
prepareMcpSocketAddress(this.address);
|
|
216
205
|
const privatePath = privatePathFor(this.socketPath);
|
|
217
206
|
await clearStalePath(privatePath);
|
|
218
207
|
const server = await this.listenPrivately(privatePath);
|
|
208
|
+
const privateInode = inodeFor(privatePath);
|
|
219
209
|
try {
|
|
220
|
-
|
|
210
|
+
if (privateInode === null) {
|
|
211
|
+
throw listenFailureError(this.socketPath, "ENOENT");
|
|
212
|
+
}
|
|
213
|
+
const publishedInode = await publishSocket(privatePath, this.socketPath);
|
|
214
|
+
if (publishedInode !== privateInode) {
|
|
215
|
+
throw listenFailureError(this.socketPath, "ESTALE");
|
|
216
|
+
}
|
|
217
|
+
this.boundInode = privateInode;
|
|
221
218
|
this.server = server;
|
|
222
219
|
}
|
|
223
220
|
catch (error) {
|
|
224
221
|
await closeServer(server);
|
|
222
|
+
if (privateInode !== null) {
|
|
223
|
+
removeEntry(privatePath, privateInode);
|
|
224
|
+
removeEntry(this.socketPath, privateInode);
|
|
225
|
+
}
|
|
225
226
|
throw error;
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
open() {
|
|
229
|
-
return withClaimLock(this.socketPath, () =>
|
|
230
|
+
return withClaimLock(this.socketPath, async () => {
|
|
231
|
+
try {
|
|
232
|
+
await this.bind();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
cleanupMcpSocketAddress(this.address);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
230
239
|
}
|
|
231
240
|
async settleStartup() {
|
|
232
241
|
const startup = this.startup;
|
|
@@ -235,12 +244,21 @@ class SocketServer {
|
|
|
235
244
|
await Promise.allSettled([startup]);
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
|
-
async release() {
|
|
247
|
+
async release(server) {
|
|
239
248
|
const inode = this.boundInode;
|
|
249
|
+
await withClaimLock(this.socketPath, async () => {
|
|
250
|
+
this.registry.dispose();
|
|
251
|
+
await closeServer(server);
|
|
252
|
+
try {
|
|
253
|
+
if (inode !== null) {
|
|
254
|
+
removeEntry(this.socketPath, inode);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
cleanupMcpSocketAddress(this.address);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
240
261
|
this.boundInode = null;
|
|
241
|
-
if (inode !== null) {
|
|
242
|
-
await releaseSocketPath(this.socketPath, inode);
|
|
243
|
-
}
|
|
244
262
|
}
|
|
245
263
|
async start() {
|
|
246
264
|
this.startup ??= this.open();
|
|
@@ -261,10 +279,8 @@ class SocketServer {
|
|
|
261
279
|
if (!server) {
|
|
262
280
|
return;
|
|
263
281
|
}
|
|
282
|
+
await this.release(server);
|
|
264
283
|
this.server = null;
|
|
265
|
-
this.registry.dispose();
|
|
266
|
-
await closeServer(server);
|
|
267
|
-
await this.release();
|
|
268
284
|
}
|
|
269
285
|
}
|
|
270
286
|
export { SocketServer };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-server.js","sourceRoot":"","sources":["../src/socket-server.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as net from \"node:net\";\nimport { basename, dirname, join, resolve as resolvePath } from \"node:path\";\nimport type { ConnectionRegistry } from \"./connection-registry.js\";\nimport { DEFAULT_SOCKET_PATH } from \"./protocol/schemas.js\";\nimport { connectionErrorEvent } from \"./transport.js\";\n\ntype ProbeOutcome = { kind: \"live\" } | { kind: \"unknown\"; code: string } | { kind: \"vacant\" };\ntype PathVerdict = ProbeOutcome | { kind: \"directory\" };\ntype ClaimOutcome = \"occupied\" | \"published\";\n\nconst PROBE_TIMEOUT_MS = 1000;\nconst PROBE_ATTEMPTS = 3;\nconst PROBE_RETRY_DELAY_MS = 50;\nconst CLAIM_ATTEMPTS = 3;\nconst CLAIM_LOCK_PREFIX = \"\\0gtkx-mcp-claim-\";\nconst CLAIM_LOCK_RETRY_DELAY_MS = 25;\nconst CLAIM_LOCK_TIMEOUT_MS = 15_000;\n\nconst delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nconst closeServer = (server: net.Server): Promise<void> =>\n new Promise((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n\nconst digestFor = (socketPath: string): string =>\n createHash(\"sha256\").update(resolvePath(socketPath)).digest(\"hex\");\n\nconst privatePathFor = (socketPath: string): string => {\n const width = Math.max(1, basename(socketPath).length - 1);\n\n return join(dirname(socketPath), `.${digestFor(socketPath).slice(0, width)}`);\n};\n\nconst bindLock = (address: string): Promise<net.Server | null> =>\n new Promise((resolve) => {\n const lock = net.createServer();\n let isBound = false;\n\n lock.on(\"error\", () => {\n if (!isBound) {\n resolve(null);\n }\n });\n\n lock.listen(address, () => {\n isBound = true;\n resolve(lock);\n });\n });\n\nconst claimBlockedError = (socketPath: string): Error =>\n new Error(\n `Timed out waiting for another GTKX MCP server to finish claiming ${socketPath}. ` +\n \"Retry once no other server is starting on that path.\",\n );\n\nconst acquireClaimLock = async (socketPath: string): Promise<net.Server | null> => {\n const address = `${CLAIM_LOCK_PREFIX}${digestFor(socketPath)}`;\n const deadline = Date.now() + CLAIM_LOCK_TIMEOUT_MS;\n let lock = await bindLock(address);\n\n while (lock === null && Date.now() < deadline) {\n await delay(CLAIM_LOCK_RETRY_DELAY_MS);\n lock = await bindLock(address);\n }\n\n return lock;\n};\n\nconst releaseClaimLock = async (lock: net.Server | null): Promise<void> => {\n if (lock) {\n await closeServer(lock);\n }\n};\n\nconst withClaimLock = async <T>(socketPath: string, action: () => Promise<T> | T): Promise<T> => {\n const lock = await acquireClaimLock(socketPath);\n\n if (lock === null) {\n throw claimBlockedError(socketPath);\n }\n\n try {\n return await action();\n } finally {\n await closeServer(lock);\n }\n};\n\nconst entryFor = (target: string): fs.Stats | null => {\n try {\n return fs.lstatSync(target);\n } catch {\n return null;\n }\n};\n\nconst inodeFor = (target: string): number | null => entryFor(target)?.ino ?? null;\n\nconst outcomeForProbeError = (error: NodeJS.ErrnoException): ProbeOutcome => {\n if (error.code === \"ECONNREFUSED\" || error.code === \"ENOENT\") {\n return { kind: \"vacant\" };\n }\n\n return { kind: \"unknown\", code: error.code ?? error.message };\n};\n\nconst probeSocket = (target: string): Promise<ProbeOutcome> =>\n new Promise((resolve) => {\n const probe = net.connect({ path: target, timeout: PROBE_TIMEOUT_MS });\n\n const settleWith = (outcome: ProbeOutcome): void => {\n probe.destroy();\n resolve(outcome);\n };\n\n probe.once(\"connect\", () => {\n settleWith({ kind: \"live\" });\n });\n\n probe.once(\"timeout\", () => {\n settleWith({ kind: \"unknown\", code: \"ETIMEDOUT\" });\n });\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n settleWith(outcomeForProbeError(error));\n });\n });\n\nconst probeUntilConclusive = async (target: string): Promise<ProbeOutcome> => {\n let outcome = await probeSocket(target);\n\n for (let attempt = 1; attempt < PROBE_ATTEMPTS && outcome.kind === \"unknown\"; attempt += 1) {\n await delay(PROBE_RETRY_DELAY_MS);\n outcome = await probeSocket(target);\n }\n\n return outcome;\n};\n\nconst alreadyOwnedError = (socketPath: string): Error =>\n new Error(\n `Another GTKX MCP server already owns ${socketPath}. ` +\n \"Stop the other server (for example, the GTKX MCP server of another active session) and reconnect.\",\n );\n\nconst undecidedOwnerError = (socketPath: string, code: string): Error =>\n new Error(\n `Could not tell whether another GTKX MCP server owns ${socketPath}: probing it failed with ${code}. ` +\n \"Leaving the socket in place instead of removing one that may still be serving another session. \" +\n \"Retry, or delete the file by hand once no server is running.\",\n );\n\nconst directoryPathError = (socketPath: string): Error =>\n new Error(\n `The GTKX MCP socket path ${socketPath} is a directory, not a socket. ` +\n \"Remove it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.\",\n );\n\nconst listenFailureError = (socketPath: string, code: string): Error =>\n new Error(\n `Could not create the GTKX MCP socket at ${socketPath}: listening failed with ${code}. ` +\n \"Check that its directory exists and is writable.\",\n );\n\nconst removeEntry = (target: string, inode: number): void => {\n if (inodeFor(target) === inode) {\n fs.rmSync(target, { force: true });\n }\n};\n\nconst clearStalePath = async (target: string): Promise<PathVerdict> => {\n const entry = entryFor(target);\n\n if (entry === null) {\n return { kind: \"vacant\" };\n }\n\n if (entry.isDirectory()) {\n return { kind: \"directory\" };\n }\n\n const outcome = await probeUntilConclusive(target);\n\n if (outcome.kind === \"vacant\") {\n removeEntry(target, entry.ino);\n }\n\n return outcome;\n};\n\nconst requireVacantPath = async (socketPath: string): Promise<void> => {\n const verdict = await clearStalePath(socketPath);\n\n if (verdict.kind === \"live\") {\n throw alreadyOwnedError(socketPath);\n }\n\n if (verdict.kind === \"directory\") {\n throw directoryPathError(socketPath);\n }\n\n if (verdict.kind === \"unknown\") {\n throw undecidedOwnerError(socketPath, verdict.code);\n }\n};\n\nconst claimSocketPath = (privatePath: string, socketPath: string): ClaimOutcome => {\n try {\n fs.linkSync(privatePath, socketPath);\n\n return \"published\";\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"EEXIST\") {\n return \"occupied\";\n }\n\n throw error;\n }\n};\n\nconst publishSocket = async (privatePath: string, socketPath: string): Promise<number | null> => {\n for (let attempt = 1; attempt <= CLAIM_ATTEMPTS; attempt += 1) {\n await requireVacantPath(socketPath);\n\n if (claimSocketPath(privatePath, socketPath) === \"published\") {\n const inode = inodeFor(socketPath);\n fs.rmSync(privatePath, { force: true });\n\n return inode;\n }\n }\n\n throw alreadyOwnedError(socketPath);\n};\n\nconst releaseSocketPath = async (socketPath: string, inode: number): Promise<void> => {\n const lock = await acquireClaimLock(socketPath);\n\n try {\n removeEntry(socketPath, inode);\n } finally {\n await releaseClaimLock(lock);\n }\n};\n\nclass SocketServer {\n private server: net.Server | null = null;\n private socketPath: string;\n private registry: ConnectionRegistry;\n private boundInode: number | null = null;\n private startup: Promise<void> | null = null;\n\n constructor(registry: ConnectionRegistry, socketPath: string = DEFAULT_SOCKET_PATH) {\n this.registry = registry;\n this.socketPath = socketPath;\n }\n\n private listen(privatePath: string): Promise<net.Server> {\n return new Promise((resolve, reject) => {\n const server = net.createServer((socket) => this.registry.register(socket));\n let isListening = false;\n\n server.on(\"error\", (error) => {\n this.registry.dispatchEvent(connectionErrorEvent(error));\n\n if (!isListening) {\n reject(error);\n }\n });\n\n server.listen(privatePath, () => {\n isListening = true;\n resolve(server);\n });\n });\n }\n\n private async listenPrivately(privatePath: string): Promise<net.Server> {\n try {\n return await this.listen(privatePath);\n } catch (error) {\n const failure = error as NodeJS.ErrnoException;\n throw listenFailureError(this.socketPath, failure.code ?? failure.message);\n }\n }\n\n private async bind(): Promise<void> {\n const privatePath = privatePathFor(this.socketPath);\n await clearStalePath(privatePath);\n const server = await this.listenPrivately(privatePath);\n\n try {\n this.boundInode = await publishSocket(privatePath, this.socketPath);\n this.server = server;\n } catch (error) {\n await closeServer(server);\n throw error;\n }\n }\n\n private open(): Promise<void> {\n return withClaimLock(this.socketPath, () => this.bind());\n }\n\n private async settleStartup(): Promise<void> {\n const startup = this.startup;\n this.startup = null;\n\n if (startup) {\n await Promise.allSettled([startup]);\n }\n }\n\n private async release(): Promise<void> {\n const inode = this.boundInode;\n this.boundInode = null;\n\n if (inode !== null) {\n await releaseSocketPath(this.socketPath, inode);\n }\n }\n\n async start(): Promise<void> {\n this.startup ??= this.open();\n const startup = this.startup;\n\n try {\n await startup;\n } catch (error) {\n if (this.startup === startup) {\n this.startup = null;\n }\n\n throw error;\n }\n }\n\n async stop(): Promise<void> {\n await this.settleStartup();\n const server = this.server;\n\n if (!server) {\n return;\n }\n\n this.server = null;\n this.registry.dispose();\n await closeServer(server);\n await this.release();\n }\n}\n\nexport { SocketServer };\n"]}
|
|
1
|
+
{"version":3,"file":"socket-server.js","sourceRoot":"","sources":["../src/socket-server.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as net from \"node:net\";\nimport { basename, dirname, join, resolve as resolvePath } from \"node:path\";\nimport type { ConnectionRegistry } from \"./connection-registry.js\";\nimport {\n cleanupMcpSocketAddress,\n type McpSocketAddress,\n prepareMcpSocketAddress,\n resolveMcpSocketAddress,\n} from \"./socket-path.js\";\nimport { connectionErrorEvent } from \"./transport.js\";\n\ntype ProbeOutcome = { kind: \"live\" } | { kind: \"unknown\"; code: string } | { kind: \"vacant\" };\ntype PathVerdict = ProbeOutcome | { kind: \"invalid\" };\ntype ClaimOutcome = \"occupied\" | \"published\";\n\nconst PROBE_TIMEOUT_MS = 1000;\nconst PROBE_ATTEMPTS = 3;\nconst PROBE_RETRY_DELAY_MS = 50;\nconst CLAIM_ATTEMPTS = 3;\nconst CLAIM_LOCK_PREFIX = \"\\0gtkx-mcp-claim-\";\nconst CLAIM_LOCK_RETRY_DELAY_MS = 25;\nconst CLAIM_LOCK_TIMEOUT_MS = 15_000;\n\nconst delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nconst closeServer = (server: net.Server): Promise<void> =>\n new Promise((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n\nconst digestFor = (socketPath: string): string =>\n createHash(\"sha256\").update(resolvePath(socketPath)).digest(\"hex\");\n\nconst privatePathFor = (socketPath: string): string => {\n const width = Math.max(1, basename(socketPath).length - 1);\n\n return join(dirname(socketPath), `.${digestFor(socketPath).slice(0, width)}`);\n};\n\nconst bindLock = (address: string): Promise<net.Server | null> =>\n new Promise((resolve) => {\n const lock = net.createServer();\n let isBound = false;\n\n lock.on(\"error\", () => {\n if (!isBound) {\n resolve(null);\n }\n });\n\n lock.listen(address, () => {\n isBound = true;\n resolve(lock);\n });\n });\n\nconst claimBlockedError = (socketPath: string): Error =>\n new Error(\n `Timed out waiting for another GTKX MCP server to finish claiming ${socketPath}. ` +\n \"Retry once no other server is starting on that path.\",\n );\n\nconst acquireClaimLock = async (socketPath: string): Promise<net.Server | null> => {\n const address = `${CLAIM_LOCK_PREFIX}${digestFor(socketPath)}`;\n const deadline = Date.now() + CLAIM_LOCK_TIMEOUT_MS;\n let lock = await bindLock(address);\n\n while (lock === null && Date.now() < deadline) {\n await delay(CLAIM_LOCK_RETRY_DELAY_MS);\n lock = await bindLock(address);\n }\n\n return lock;\n};\n\nconst withClaimLock = async <T>(socketPath: string, action: () => Promise<T> | T): Promise<T> => {\n const lock = await acquireClaimLock(socketPath);\n\n if (lock === null) {\n throw claimBlockedError(socketPath);\n }\n\n try {\n return await action();\n } finally {\n await closeServer(lock);\n }\n};\n\nconst entryFor = (target: string): fs.Stats | null => {\n try {\n return fs.lstatSync(target);\n } catch {\n return null;\n }\n};\n\nconst inodeFor = (target: string): number | null => entryFor(target)?.ino ?? null;\n\nconst outcomeForProbeError = (error: NodeJS.ErrnoException): ProbeOutcome => {\n if (error.code === \"ECONNREFUSED\" || error.code === \"ENOENT\") {\n return { kind: \"vacant\" };\n }\n\n return { kind: \"unknown\", code: error.code ?? error.message };\n};\n\nconst probeSocket = (target: string): Promise<ProbeOutcome> =>\n new Promise((resolve) => {\n const probe = net.connect({ path: target, timeout: PROBE_TIMEOUT_MS });\n\n const settleWith = (outcome: ProbeOutcome): void => {\n probe.destroy();\n resolve(outcome);\n };\n\n probe.once(\"connect\", () => {\n settleWith({ kind: \"live\" });\n });\n\n probe.once(\"timeout\", () => {\n settleWith({ kind: \"unknown\", code: \"ETIMEDOUT\" });\n });\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n settleWith(outcomeForProbeError(error));\n });\n });\n\nconst probeUntilConclusive = async (target: string): Promise<ProbeOutcome> => {\n let outcome = await probeSocket(target);\n\n for (let attempt = 1; attempt < PROBE_ATTEMPTS && outcome.kind === \"unknown\"; attempt += 1) {\n await delay(PROBE_RETRY_DELAY_MS);\n outcome = await probeSocket(target);\n }\n\n return outcome;\n};\n\nconst alreadyOwnedError = (socketPath: string): Error =>\n new Error(\n `Another GTKX MCP server already owns ${socketPath}. ` +\n \"Stop the other server (for example, the GTKX MCP server of another active session) and reconnect.\",\n );\n\nconst undecidedOwnerError = (socketPath: string, code: string): Error =>\n new Error(\n `Could not tell whether another GTKX MCP server owns ${socketPath}: probing it failed with ${code}. ` +\n \"Leaving the socket in place instead of removing one that may still be serving another session. \" +\n \"Retry, or delete the file by hand once no server is running.\",\n );\n\nconst invalidPathError = (socketPath: string): Error =>\n new Error(\n `The GTKX MCP socket path ${socketPath} exists and is not a socket. ` +\n \"Move it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.\",\n );\n\nconst listenFailureError = (socketPath: string, code: string): Error =>\n new Error(\n `Could not create the GTKX MCP socket at ${socketPath}: listening failed with ${code}. ` +\n \"Check that its directory exists and is writable.\",\n );\n\nconst removeEntry = (target: string, inode: number): void => {\n if (inodeFor(target) === inode) {\n fs.rmSync(target, { force: true });\n }\n};\n\nconst clearStalePath = async (target: string): Promise<PathVerdict> => {\n const entry = entryFor(target);\n\n if (entry === null) {\n return { kind: \"vacant\" };\n }\n\n if (!entry.isSocket()) {\n return { kind: \"invalid\" };\n }\n\n const outcome = await probeUntilConclusive(target);\n\n if (outcome.kind === \"vacant\") {\n removeEntry(target, entry.ino);\n }\n\n return outcome;\n};\n\nconst requireVacantPath = async (socketPath: string): Promise<void> => {\n const verdict = await clearStalePath(socketPath);\n\n if (verdict.kind === \"live\") {\n throw alreadyOwnedError(socketPath);\n }\n\n if (verdict.kind === \"invalid\") {\n throw invalidPathError(socketPath);\n }\n\n if (verdict.kind === \"unknown\") {\n throw undecidedOwnerError(socketPath, verdict.code);\n }\n};\n\nconst claimSocketPath = (privatePath: string, socketPath: string): ClaimOutcome => {\n try {\n fs.linkSync(privatePath, socketPath);\n\n return \"published\";\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"EEXIST\") {\n return \"occupied\";\n }\n\n throw error;\n }\n};\n\nconst publishSocket = async (privatePath: string, socketPath: string): Promise<number | null> => {\n for (let attempt = 1; attempt <= CLAIM_ATTEMPTS; attempt += 1) {\n await requireVacantPath(socketPath);\n\n if (claimSocketPath(privatePath, socketPath) === \"published\") {\n const inode = inodeFor(socketPath);\n fs.rmSync(privatePath, { force: true });\n\n return inode;\n }\n }\n\n throw alreadyOwnedError(socketPath);\n};\n\nclass SocketServer {\n private server: net.Server | null = null;\n private socketPath: string;\n private registry: ConnectionRegistry;\n private address: McpSocketAddress;\n private boundInode: number | null = null;\n private startup: Promise<void> | null = null;\n\n constructor(registry: ConnectionRegistry, address: McpSocketAddress = resolveMcpSocketAddress()) {\n this.registry = registry;\n this.address = address;\n this.socketPath = address.path;\n }\n\n private listen(privatePath: string): Promise<net.Server> {\n return new Promise((resolve, reject) => {\n const server = net.createServer((socket) => this.registry.register(socket));\n let isListening = false;\n\n server.on(\"error\", (error) => {\n this.registry.dispatchEvent(connectionErrorEvent(error));\n\n if (!isListening) {\n reject(error);\n }\n });\n\n server.listen(privatePath, () => {\n isListening = true;\n resolve(server);\n });\n });\n }\n\n private async listenPrivately(privatePath: string): Promise<net.Server> {\n try {\n return await this.listen(privatePath);\n } catch (error) {\n const failure = error as NodeJS.ErrnoException;\n throw listenFailureError(this.socketPath, failure.code ?? failure.message);\n }\n }\n\n private async bind(): Promise<void> {\n prepareMcpSocketAddress(this.address);\n const privatePath = privatePathFor(this.socketPath);\n await clearStalePath(privatePath);\n const server = await this.listenPrivately(privatePath);\n const privateInode = inodeFor(privatePath);\n\n try {\n if (privateInode === null) {\n throw listenFailureError(this.socketPath, \"ENOENT\");\n }\n\n const publishedInode = await publishSocket(privatePath, this.socketPath);\n\n if (publishedInode !== privateInode) {\n throw listenFailureError(this.socketPath, \"ESTALE\");\n }\n\n this.boundInode = privateInode;\n this.server = server;\n } catch (error) {\n await closeServer(server);\n\n if (privateInode !== null) {\n removeEntry(privatePath, privateInode);\n removeEntry(this.socketPath, privateInode);\n }\n\n throw error;\n }\n }\n\n private open(): Promise<void> {\n return withClaimLock(this.socketPath, async () => {\n try {\n await this.bind();\n } catch (error) {\n cleanupMcpSocketAddress(this.address);\n throw error;\n }\n });\n }\n\n private async settleStartup(): Promise<void> {\n const startup = this.startup;\n this.startup = null;\n\n if (startup) {\n await Promise.allSettled([startup]);\n }\n }\n\n private async release(server: net.Server): Promise<void> {\n const inode = this.boundInode;\n\n await withClaimLock(this.socketPath, async () => {\n this.registry.dispose();\n await closeServer(server);\n\n try {\n if (inode !== null) {\n removeEntry(this.socketPath, inode);\n }\n } finally {\n cleanupMcpSocketAddress(this.address);\n }\n });\n\n this.boundInode = null;\n }\n\n async start(): Promise<void> {\n this.startup ??= this.open();\n const startup = this.startup;\n\n try {\n await startup;\n } catch (error) {\n if (this.startup === startup) {\n this.startup = null;\n }\n\n throw error;\n }\n }\n\n async stop(): Promise<void> {\n await this.settleStartup();\n const server = this.server;\n\n if (!server) {\n return;\n }\n\n await this.release(server);\n this.server = null;\n }\n}\n\nexport { SocketServer };\n"]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/mcp",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
4
|
-
"description": "MCP server
|
|
3
|
+
"version": "2.0.0-beta.10",
|
|
4
|
+
"description": "MCP server for GTKX GNOME references and live Adwaita apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
7
7
|
"gtk",
|
|
8
8
|
"gtk4",
|
|
9
|
+
"adwaita",
|
|
10
|
+
"gnome",
|
|
9
11
|
"mcp",
|
|
10
12
|
"model-context-protocol",
|
|
11
13
|
"ai",
|
|
@@ -63,15 +65,15 @@
|
|
|
63
65
|
"node": ">=26.7.0"
|
|
64
66
|
},
|
|
65
67
|
"dependencies": {
|
|
66
|
-
"@gtkx/codegen": "2.0.0-beta.
|
|
67
|
-
"@gtkx/config": "2.0.0-beta.
|
|
68
|
-
"@gtkx/utils": "2.0.0-beta.
|
|
68
|
+
"@gtkx/codegen": "2.0.0-beta.10",
|
|
69
|
+
"@gtkx/config": "2.0.0-beta.10",
|
|
70
|
+
"@gtkx/utils": "2.0.0-beta.10",
|
|
69
71
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
70
72
|
"zod": "^4.5.4"
|
|
71
73
|
},
|
|
72
74
|
"devDependencies": {
|
|
73
|
-
"@gtkx/vitest": "2.0.0-beta.
|
|
74
|
-
"vitest": "^
|
|
75
|
+
"@gtkx/vitest": "2.0.0-beta.10",
|
|
76
|
+
"vitest": "^5.0.0"
|
|
75
77
|
},
|
|
76
78
|
"scripts": {
|
|
77
79
|
"release": "tsx ../../scripts/release-package.ts"
|
package/src/app-router.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { type AppInfo, RegisterParamsSchema } from "./protocol/schemas.js";
|
|
|
16
16
|
|
|
17
17
|
type AppRegisteredEvent = CustomEvent<AppInfo>;
|
|
18
18
|
type AppUnregisteredEvent = CustomEvent<string>;
|
|
19
|
+
type PendingAppWait = { reject: (error: Error) => void };
|
|
19
20
|
|
|
20
21
|
type RegisteredApp = {
|
|
21
22
|
info: AppInfo;
|
|
@@ -24,6 +25,14 @@ type RegisteredApp = {
|
|
|
24
25
|
|
|
25
26
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
26
27
|
|
|
28
|
+
const routerStoppedError = (): Error => new Error("GTKX MCP server stopped while waiting for an application");
|
|
29
|
+
|
|
30
|
+
const appWaitTimeoutError = (timeout: number): Error =>
|
|
31
|
+
new Error(
|
|
32
|
+
`Timeout waiting for app registration after ${String(timeout)}ms. ` +
|
|
33
|
+
"Make sure your GTKX app is running with 'gtkx dev'.",
|
|
34
|
+
);
|
|
35
|
+
|
|
27
36
|
function appRegisteredEvent(info: AppInfo): AppRegisteredEvent {
|
|
28
37
|
return new CustomEvent("appRegistered", { detail: info });
|
|
29
38
|
}
|
|
@@ -43,6 +52,10 @@ class AppRouter extends EventTarget {
|
|
|
43
52
|
|
|
44
53
|
private connections: AppConnections;
|
|
45
54
|
|
|
55
|
+
private pendingAppWaits: Set<PendingAppWait> = new Set();
|
|
56
|
+
|
|
57
|
+
private isDisposed = false;
|
|
58
|
+
|
|
46
59
|
constructor(connections: AppConnections, options: { requestTimeout?: number } = {}) {
|
|
47
60
|
super();
|
|
48
61
|
this.connections = connections;
|
|
@@ -69,6 +82,10 @@ class AppRouter extends EventTarget {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
private handleRequest(connection: ProtocolConnection, request: JSONRPCRequest): Promise<Result> {
|
|
85
|
+
if (this.isDisposed) {
|
|
86
|
+
return Promise.reject(routerStoppedError());
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
if (request.method === "app.register") {
|
|
73
90
|
return Promise.resolve(this.handleRegister(connection, request));
|
|
74
91
|
}
|
|
@@ -134,6 +151,40 @@ class AppRouter extends EventTarget {
|
|
|
134
151
|
this.dispatchEvent(appUnregisteredEvent(applicationId));
|
|
135
152
|
}
|
|
136
153
|
|
|
154
|
+
private async waitForRegistration(applicationId: string | undefined, timeout: number): Promise<AppInfo> {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const { promise, reject, resolve } = Promise.withResolvers<AppInfo>();
|
|
157
|
+
const rejectWait = (error: Error): void => {
|
|
158
|
+
controller.abort();
|
|
159
|
+
reject(error);
|
|
160
|
+
};
|
|
161
|
+
const pendingWait: PendingAppWait = { reject: rejectWait };
|
|
162
|
+
const timeoutId = setTimeout(() => {
|
|
163
|
+
rejectWait(appWaitTimeoutError(timeout));
|
|
164
|
+
}, timeout);
|
|
165
|
+
|
|
166
|
+
this.addEventListener(
|
|
167
|
+
"appRegistered",
|
|
168
|
+
(event) => {
|
|
169
|
+
const info = (event as AppRegisteredEvent).detail;
|
|
170
|
+
|
|
171
|
+
if (applicationId === undefined || info.applicationId === applicationId) {
|
|
172
|
+
resolve(info);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{ signal: controller.signal },
|
|
176
|
+
);
|
|
177
|
+
this.pendingAppWaits.add(pendingWait);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
return await promise;
|
|
181
|
+
} finally {
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
controller.abort();
|
|
184
|
+
this.pendingAppWaits.delete(pendingWait);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
137
188
|
getApps(): AppInfo[] {
|
|
138
189
|
return this.apps.values().map((app) => app.info).toArray();
|
|
139
190
|
}
|
|
@@ -152,36 +203,40 @@ class AppRouter extends EventTarget {
|
|
|
152
203
|
return this.getDefaultApp()?.info.projectRoot;
|
|
153
204
|
}
|
|
154
205
|
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
dispose(): void {
|
|
207
|
+
if (this.isDisposed) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.isDisposed = true;
|
|
212
|
+
const error = routerStoppedError();
|
|
157
213
|
|
|
158
|
-
|
|
159
|
-
|
|
214
|
+
for (const pendingWait of this.pendingAppWaits) {
|
|
215
|
+
pendingWait.reject(error);
|
|
160
216
|
}
|
|
217
|
+
}
|
|
161
218
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
219
|
+
waitForApp(applicationId?: string, timeout: number = AppRouter.defaultWaitTimeout): Promise<AppInfo> {
|
|
220
|
+
if (this.isDisposed) {
|
|
221
|
+
return Promise.reject(routerStoppedError());
|
|
222
|
+
}
|
|
165
223
|
|
|
166
|
-
|
|
167
|
-
new Error(
|
|
168
|
-
`Timeout waiting for app registration after ${String(timeout)}ms. ` +
|
|
169
|
-
"Make sure your GTKX app is running with 'gtkx dev'.",
|
|
170
|
-
),
|
|
171
|
-
);
|
|
172
|
-
}, timeout);
|
|
224
|
+
const app = applicationId === undefined ? this.getDefaultApp() : this.apps.get(applicationId);
|
|
173
225
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
resolve((event as AppRegisteredEvent).detail);
|
|
178
|
-
};
|
|
226
|
+
if (app) {
|
|
227
|
+
return Promise.resolve(app.info);
|
|
228
|
+
}
|
|
179
229
|
|
|
180
|
-
|
|
181
|
-
});
|
|
230
|
+
return this.waitForRegistration(applicationId, timeout);
|
|
182
231
|
}
|
|
183
232
|
|
|
184
|
-
async sendToApp<T>(
|
|
233
|
+
async sendToApp<T>(
|
|
234
|
+
applicationId: string | undefined,
|
|
235
|
+
method: string,
|
|
236
|
+
params?: RequestParams,
|
|
237
|
+
waitTimeout?: number,
|
|
238
|
+
): Promise<T> {
|
|
239
|
+
await this.waitForApp(applicationId, waitTimeout);
|
|
185
240
|
const app = this.resolveTargetApp(applicationId);
|
|
186
241
|
|
|
187
242
|
try {
|
package/src/internal.ts
CHANGED
|
@@ -6,7 +6,6 @@ export {
|
|
|
6
6
|
widgetNotFoundError,
|
|
7
7
|
} from "./protocol/errors.js";
|
|
8
8
|
export {
|
|
9
|
-
DEFAULT_SOCKET_PATH,
|
|
10
9
|
DEFAULT_SUBTREE_DEPTH,
|
|
11
10
|
MAX_SUBTREE_WIDGETS,
|
|
12
11
|
type ParamsSchema,
|
|
@@ -16,5 +15,9 @@ export {
|
|
|
16
15
|
type ServerRequestParams,
|
|
17
16
|
ServerRequestParamsSchemas,
|
|
18
17
|
} from "./protocol/schemas.js";
|
|
18
|
+
export {
|
|
19
|
+
MCP_SOCKET_PATH_ENV,
|
|
20
|
+
resolveMcpSocketPath,
|
|
21
|
+
} from "./socket-path.js";
|
|
19
22
|
export { ProtocolConnection } from "./transport.js";
|
|
20
23
|
export type { JSONRPCRequest, Result } from "@modelcontextprotocol/sdk/types.js";
|
package/src/protocol/schemas.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { tmpdir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { z } from "zod";
|
|
4
2
|
|
|
5
3
|
type SerializedWidget = {
|
|
@@ -130,12 +128,6 @@ const ServerRequestParamsSchemas: {
|
|
|
130
128
|
"widget.screenshot": screenshotParams,
|
|
131
129
|
};
|
|
132
130
|
|
|
133
|
-
const DEFAULT_SOCKET_PATH: string = join(getRuntimeDir(), "gtkx-mcp.sock");
|
|
134
|
-
|
|
135
|
-
function getRuntimeDir(): string {
|
|
136
|
-
return process.env.XDG_RUNTIME_DIR ?? tmpdir();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
131
|
export {
|
|
140
132
|
DEFAULT_SUBTREE_DEPTH,
|
|
141
133
|
MAX_SUBTREE_WIDGETS,
|
|
@@ -148,7 +140,6 @@ export {
|
|
|
148
140
|
fireEventParams,
|
|
149
141
|
screenshotParams,
|
|
150
142
|
ServerRequestParamsSchemas,
|
|
151
|
-
DEFAULT_SOCKET_PATH,
|
|
152
143
|
type SerializedWidget,
|
|
153
144
|
type SerializedProperty,
|
|
154
145
|
type AppInfo,
|