@gtkx/mcp 2.0.0-beta.1 → 2.0.0-beta.11
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 +6 -169
- package/dist/app-router.d.ts +7 -7
- package/dist/app-router.d.ts.map +1 -1
- package/dist/app-router.js +63 -32
- package/dist/app-router.js.map +1 -1
- package/dist/connection-registry.d.ts +0 -1
- package/dist/connection-registry.d.ts.map +1 -1
- package/dist/connection-registry.js +0 -3
- package/dist/connection-registry.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 +2 -7
- 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 +32 -18
- package/dist/reference.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +110 -62
- 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/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +1 -3
- package/dist/tool-filter.js.map +1 -1
- package/package.json +22 -8
- package/src/app-router.ts +84 -39
- package/src/connection-registry.ts +0 -3
- package/src/internal.ts +4 -1
- package/src/protocol/schemas.ts +1 -14
- package/src/reference.ts +48 -19
- package/src/server.ts +133 -64
- package/src/socket-path.ts +103 -0
- package/src/socket-server.ts +61 -37
- package/src/tool-filter.ts +1 -4
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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAE5E,OAAO,EACH,uBAAuB,EAEvB,uBAAuB,EACvB,uBAAuB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAMtD,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAC9B,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAChC,MAAM,cAAc,GAAG,CAAC,CAAC;AACzB,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAC9C,MAAM,yBAAyB,GAAG,EAAE,CAAC;AACrC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAErC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CACxC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACpB,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC,CAAC;AAEP,MAAM,WAAW,GAAG,CAAC,MAAkB,EAAiB,EAAE,CACtD,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACpB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;QACd,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEP,MAAM,SAAS,GAAG,CAAC,UAAkB,EAAU,EAAE,CAC7C,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEvE,MAAM,cAAc,GAAG,CAAC,UAAkB,EAAU,EAAE;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE3D,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;AAClF,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,OAAe,EAA8B,EAAE,CAC7D,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACpB,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;IAChC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAClB,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE;QACtB,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEP,MAAM,iBAAiB,GAAG,CAAC,UAAkB,EAAS,EAAE,CACpD,IAAI,KAAK,CACL,oEAAoE,UAAU,IAAI;IAClF,sDAAsD,CACzD,CAAC;AAEN,MAAM,gBAAgB,GAAG,KAAK,EAAE,UAAkB,EAA8B,EAAE;IAC9E,MAAM,OAAO,GAAG,GAAG,iBAAiB,GAAG,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;IAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,qBAAqB,CAAC;IACpD,IAAI,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;IAEnC,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC5C,MAAM,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACvC,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,KAAK,EAAK,UAAkB,EAAE,MAA4B,EAAc,EAAE;IAC5F,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAEhD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAChB,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,CAAC;QACD,OAAO,MAAM,MAAM,EAAE,CAAC;IAC1B,CAAC;YAAS,CAAC;QACP,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAmB,EAAE;IACjD,IAAI,CAAC;QACD,OAAO,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAiB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC;AAElF,MAAM,oBAAoB,GAAG,CAAC,KAA4B,EAAgB,EAAE;IACxE,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;AAClE,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,MAAc,EAAyB,EAAE,CAC1D,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACpB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,CAAC,OAAqB,EAAQ,EAAE;QAC/C,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,OAAO,CAAC,OAAO,CAAC,CAAC;IACrB,CAAC,CAAC;IAEF,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;QACjD,UAAU,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEP,MAAM,oBAAoB,GAAG,KAAK,EAAE,MAAc,EAAyB,EAAE;IACzE,IAAI,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAExC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,cAAc,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACzF,MAAM,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAClC,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CAAC,UAAkB,EAAS,EAAE,CACpD,IAAI,KAAK,CACL,wCAAwC,UAAU,IAAI;IACtD,mGAAmG,CACtG,CAAC;AAEN,MAAM,mBAAmB,GAAG,CAAC,UAAkB,EAAE,IAAY,EAAS,EAAE,CACpE,IAAI,KAAK,CACL,uDAAuD,UAAU,4BAA4B,IAAI,IAAI;IACrG,iGAAiG;IACjG,8DAA8D,CACjE,CAAC;AAEN,MAAM,gBAAgB,GAAG,CAAC,UAAkB,EAAS,EAAE,CACnD,IAAI,KAAK,CACL,4BAA4B,UAAU,+BAA+B;IACrE,oFAAoF,CACvF,CAAC;AAEN,MAAM,kBAAkB,GAAG,CAAC,UAAkB,EAAE,IAAY,EAAS,EAAE,CACnE,IAAI,KAAK,CACL,2CAA2C,UAAU,2BAA2B,IAAI,IAAI;IACxF,kDAAkD,CACrD,CAAC;AAEN,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,KAAa,EAAQ,EAAE;IACxD,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC;QAC7B,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,KAAK,EAAE,MAAc,EAAwB,EAAE;IAClE,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE/B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAEnD,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5B,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,KAAK,EAAE,UAAkB,EAAiB,EAAE;IAClE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,CAAC;IAEjD,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,WAAmB,EAAE,UAAkB,EAAgB,EAAE;IAC9E,IAAI,CAAC;QACD,EAAE,CAAC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAErC,OAAO,WAAW,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO,UAAU,CAAC;QACtB,CAAC;QAED,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,KAAK,EAAE,WAAmB,EAAE,UAAkB,EAA0B,EAAE;IAC5F,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,cAAc,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC5D,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAEpC,IAAI,eAAe,CAAC,WAAW,EAAE,UAAU,CAAC,KAAK,WAAW,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACnC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAExC,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAED,MAAM,iBAAiB,CAAC,UAAU,CAAC,CAAC;AACxC,CAAC,CAAC;AAEF,MAAM,YAAY;IACN,MAAM,GAAsB,IAAI,CAAC;IACjC,UAAU,CAAS;IACnB,QAAQ,CAAqB;IAC7B,OAAO,CAAmB;IAC1B,UAAU,GAAkB,IAAI,CAAC;IACjC,OAAO,GAAyB,IAAI,CAAC;IAE7C,YAAY,QAA4B,EAAE,UAA4B,uBAAuB,EAAE;QAC3F,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IACnC,CAAC;IAEO,MAAM,CAAC,WAAmB;QAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5E,IAAI,WAAW,GAAG,KAAK,CAAC;YAExB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;gBACzB,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;gBAEzD,IAAI,CAAC,WAAW,EAAE,CAAC;oBACf,MAAM,CAAC,KAAK,CAAC,CAAC;gBAClB,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE;gBAC5B,WAAW,GAAG,IAAI,CAAC;gBACnB,OAAO,CAAC,MAAM,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,WAAmB;QAC7C,IAAI,CAAC;YACD,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,KAA8B,CAAC;YAC/C,MAAM,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/E,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,IAAI;QACd,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACvD,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;QAE3C,IAAI,CAAC;YACD,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;gBACxB,MAAM,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACxD,CAAC;YAED,MAAM,cAAc,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YAEzE,IAAI,cAAc,KAAK,YAAY,EAAE,CAAC;gBAClC,MAAM,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACxD,CAAC;YAED,IAAI,CAAC,UAAU,GAAG,YAAY,CAAC;YAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;YAE1B,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;gBACxB,WAAW,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;gBACvC,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;YAC/C,CAAC;YAED,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAEO,IAAI;QACR,OAAO,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;YAC7C,IAAI,CAAC;gBACD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;YACtB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACtC,MAAM,KAAK,CAAC;YAChB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,KAAK,CAAC,aAAa;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAEpB,IAAI,OAAO,EAAE,CAAC;YACV,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACxC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,MAAkB;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;QAE9B,MAAM,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;YAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACxB,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;YAE1B,IAAI,CAAC;gBACD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACjB,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;gBACxC,CAAC;YACL,CAAC;oBAAS,CAAC;gBACP,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1C,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,KAAK;QACP,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAE7B,IAAI,CAAC;YACD,MAAM,OAAO,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;gBAC3B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACxB,CAAC;YAED,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI;QACN,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAE3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO;QACX,CAAC;QAED,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACvB,CAAC;CACJ;AAED,OAAO,EAAE,YAAY,EAAE,CAAC","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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-filter.d.ts","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-filter.d.ts","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAqCtC,QAAA,MAAM,WAAW,GAAI,OAAO,IAAI,EAAE,EAAE,UAAU,WAAW,KAAG,IAAI,EAU/D,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/tool-filter.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const NEGATION_PREFIX = "!";
|
|
2
|
-
const
|
|
3
|
-
const escapeLiteral = (value) => value.replaceAll(SPECIAL_CHARACTERS, String.raw `\$&`);
|
|
4
|
-
const patternToRegExp = (pattern) => new RegExp(`^${pattern.split("*").map((part) => escapeLiteral(part)).join(".*")}$`);
|
|
2
|
+
const patternToRegExp = (pattern) => new RegExp(`^${pattern.split("*").map((part) => RegExp.escape(part)).join(".*")}$`);
|
|
5
3
|
const matchingNames = (tools, pattern) => {
|
|
6
4
|
const expression = patternToRegExp(pattern);
|
|
7
5
|
return tools.filter((tool) => expression.test(tool.name)).map((tool) => tool.name);
|
package/dist/tool-filter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-filter.js","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"AAGA,MAAM,eAAe,GAAG,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-filter.js","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"AAGA,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,MAAM,eAAe,GAAG,CAAC,OAAe,EAAU,EAAE,CAChD,IAAI,MAAM,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAExF,MAAM,aAAa,GAAG,CAAC,KAAa,EAAE,OAAe,EAAY,EAAE;IAC/D,MAAM,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAE5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvF,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,QAAqB,EAAE,KAAa,EAAE,OAAe,EAAQ,EAAE;IACjF,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAEhG,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,SAAS,EAAE,CAAC;YACZ,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACJ,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACL,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,KAAa,EAAE,QAAkB,EAAe,EAAE;IACnE,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;IACvF,MAAM,QAAQ,GAAgB,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAE3F,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,KAAa,EAAE,QAAqB,EAAU,EAAE;IACjE,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAE/F,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,CAAC","sourcesContent":["import type { McpSettings } from \"@gtkx/config/internal\";\nimport type { Tool } from \"./tool.js\";\n\nconst NEGATION_PREFIX = \"!\";\n\nconst patternToRegExp = (pattern: string): RegExp =>\n new RegExp(`^${pattern.split(\"*\").map((part) => RegExp.escape(part)).join(\".*\")}$`);\n\nconst matchingNames = (tools: Tool[], pattern: string): string[] => {\n const expression = patternToRegExp(pattern);\n\n return tools.filter((tool) => expression.test(tool.name)).map((tool) => tool.name);\n};\n\nconst applyPattern = (selected: Set<string>, tools: Tool[], pattern: string): void => {\n const isNegated = pattern.startsWith(NEGATION_PREFIX);\n const names = matchingNames(tools, isNegated ? pattern.slice(NEGATION_PREFIX.length) : pattern);\n\n for (const name of names) {\n if (isNegated) {\n selected.delete(name);\n } else {\n selected.add(name);\n }\n }\n};\n\nconst selectNames = (tools: Tool[], patterns: string[]): Set<string> => {\n const isSubtractive = patterns.every((pattern) => pattern.startsWith(NEGATION_PREFIX));\n const selected: Set<string> = new Set(isSubtractive ? tools.map((tool) => tool.name) : []);\n\n for (const pattern of patterns) {\n applyPattern(selected, tools, pattern);\n }\n\n return selected;\n};\n\nconst selectTools = (tools: Tool[], settings: McpSettings): Tool[] => {\n const allowed = settings.isReadOnly ? tools.filter((tool) => tool.kind === \"readOnly\") : tools;\n\n if (settings.tools.length === 0) {\n return allowed;\n }\n\n const selected = selectNames(allowed, settings.tools);\n\n return allowed.filter((tool) => selected.has(tool.name));\n};\n\nexport { selectTools };\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
|
|
3
|
+
"version": "2.0.0-beta.11",
|
|
4
|
+
"description": "MCP tools for GTKX documentation and running applications.",
|
|
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",
|
|
@@ -56,6 +58,18 @@
|
|
|
56
58
|
"cwd": ".",
|
|
57
59
|
"command": "eslint {projectRoot}"
|
|
58
60
|
}
|
|
61
|
+
},
|
|
62
|
+
"test": {
|
|
63
|
+
"dependsOn": [
|
|
64
|
+
"^build",
|
|
65
|
+
"gtkx:codegen",
|
|
66
|
+
"@gtkx/cli:build",
|
|
67
|
+
"@gtkx/components:build"
|
|
68
|
+
],
|
|
69
|
+
"inputs": [
|
|
70
|
+
"...",
|
|
71
|
+
"{workspaceRoot}/packages/cli/tests/fixtures/**/*"
|
|
72
|
+
]
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
},
|
|
@@ -63,15 +77,15 @@
|
|
|
63
77
|
"node": ">=26.7.0"
|
|
64
78
|
},
|
|
65
79
|
"dependencies": {
|
|
66
|
-
"@gtkx/codegen": "2.0.0-beta.
|
|
67
|
-
"@gtkx/config": "2.0.0-beta.
|
|
68
|
-
"@gtkx/utils": "2.0.0-beta.
|
|
80
|
+
"@gtkx/codegen": "2.0.0-beta.11",
|
|
81
|
+
"@gtkx/config": "2.0.0-beta.11",
|
|
82
|
+
"@gtkx/utils": "2.0.0-beta.11",
|
|
69
83
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
70
|
-
"zod": "^4.5
|
|
84
|
+
"zod": "^4.6.5"
|
|
71
85
|
},
|
|
72
86
|
"devDependencies": {
|
|
73
|
-
"@gtkx/vitest": "2.0.0-beta.
|
|
74
|
-
"vitest": "^
|
|
87
|
+
"@gtkx/vitest": "2.0.0-beta.11",
|
|
88
|
+
"vitest": "^5.0.1"
|
|
75
89
|
},
|
|
76
90
|
"scripts": {
|
|
77
91
|
"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;
|
|
@@ -23,6 +24,15 @@ type RegisteredApp = {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
27
|
+
const DEFAULT_APP_TIMEOUT_MS = 10_000;
|
|
28
|
+
|
|
29
|
+
const routerStoppedError = (): Error => new Error("GTKX MCP server stopped while waiting for an application");
|
|
30
|
+
|
|
31
|
+
const appWaitTimeoutError = (timeout: number): Error =>
|
|
32
|
+
new Error(
|
|
33
|
+
`Timeout waiting for app registration after ${String(timeout)}ms. ` +
|
|
34
|
+
"Make sure your GTKX app is running with 'gtkx dev'.",
|
|
35
|
+
);
|
|
26
36
|
|
|
27
37
|
function appRegisteredEvent(info: AppInfo): AppRegisteredEvent {
|
|
28
38
|
return new CustomEvent("appRegistered", { detail: info });
|
|
@@ -33,20 +43,19 @@ function appUnregisteredEvent(applicationId: string): AppUnregisteredEvent {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
class AppRouter extends EventTarget {
|
|
36
|
-
private static defaultWaitTimeout = 10_000;
|
|
37
|
-
|
|
38
46
|
private apps: Map<string, RegisteredApp> = new Map();
|
|
39
47
|
|
|
40
48
|
private connectionToApp: Map<string, string> = new Map();
|
|
41
49
|
|
|
42
|
-
private requestTimeout: number;
|
|
43
|
-
|
|
44
50
|
private connections: AppConnections;
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
private pendingAppWaits: Set<PendingAppWait> = new Set();
|
|
53
|
+
|
|
54
|
+
private isDisposed = false;
|
|
55
|
+
|
|
56
|
+
constructor(connections: AppConnections) {
|
|
47
57
|
super();
|
|
48
58
|
this.connections = connections;
|
|
49
|
-
this.requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
50
59
|
this.connections.onRequest = (connection, request) => this.handleRequest(connection, request);
|
|
51
60
|
|
|
52
61
|
this.connections.addEventListener("disconnection", (event) => {
|
|
@@ -69,6 +78,10 @@ class AppRouter extends EventTarget {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
private handleRequest(connection: ProtocolConnection, request: JSONRPCRequest): Promise<Result> {
|
|
81
|
+
if (this.isDisposed) {
|
|
82
|
+
return Promise.reject(routerStoppedError());
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
if (request.method === "app.register") {
|
|
73
86
|
return Promise.resolve(this.handleRegister(connection, request));
|
|
74
87
|
}
|
|
@@ -90,16 +103,10 @@ class AppRouter extends EventTarget {
|
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
const params = parseResult.data;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
applicationId: params.applicationId,
|
|
96
|
-
pid: params.pid,
|
|
97
|
-
...(params.projectRoot !== undefined && { projectRoot: params.projectRoot }),
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
this.apps.set(params.applicationId, { info: appInfo, connection });
|
|
106
|
+
this.removeApp(connection);
|
|
107
|
+
this.apps.set(params.applicationId, { info: params, connection });
|
|
101
108
|
this.connectionToApp.set(connection.id, params.applicationId);
|
|
102
|
-
this.dispatchEvent(appRegisteredEvent(
|
|
109
|
+
this.dispatchEvent(appRegisteredEvent(params));
|
|
103
110
|
|
|
104
111
|
return { success: true };
|
|
105
112
|
}
|
|
@@ -112,7 +119,7 @@ class AppRouter extends EventTarget {
|
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
if (error.code === REQUEST_TIMEOUT_CODE) {
|
|
115
|
-
return requestTimeoutError(
|
|
122
|
+
return requestTimeoutError(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
return protocolErrorFrom(error);
|
|
@@ -134,6 +141,40 @@ class AppRouter extends EventTarget {
|
|
|
134
141
|
this.dispatchEvent(appUnregisteredEvent(applicationId));
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
private async waitForRegistration(applicationId: string | undefined, timeout: number): Promise<AppInfo> {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const { promise, reject, resolve } = Promise.withResolvers<AppInfo>();
|
|
147
|
+
const rejectWait = (error: Error): void => {
|
|
148
|
+
controller.abort();
|
|
149
|
+
reject(error);
|
|
150
|
+
};
|
|
151
|
+
const pendingWait: PendingAppWait = { reject: rejectWait };
|
|
152
|
+
const timeoutId = setTimeout(() => {
|
|
153
|
+
rejectWait(appWaitTimeoutError(timeout));
|
|
154
|
+
}, timeout);
|
|
155
|
+
|
|
156
|
+
this.addEventListener(
|
|
157
|
+
"appRegistered",
|
|
158
|
+
(event) => {
|
|
159
|
+
const info = (event as AppRegisteredEvent).detail;
|
|
160
|
+
|
|
161
|
+
if (applicationId === undefined || info.applicationId === applicationId) {
|
|
162
|
+
resolve(info);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ signal: controller.signal },
|
|
166
|
+
);
|
|
167
|
+
this.pendingAppWaits.add(pendingWait);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return await promise;
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
controller.abort();
|
|
174
|
+
this.pendingAppWaits.delete(pendingWait);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
137
178
|
getApps(): AppInfo[] {
|
|
138
179
|
return this.apps.values().map((app) => app.info).toArray();
|
|
139
180
|
}
|
|
@@ -152,40 +193,44 @@ class AppRouter extends EventTarget {
|
|
|
152
193
|
return this.getDefaultApp()?.info.projectRoot;
|
|
153
194
|
}
|
|
154
195
|
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
dispose(): void {
|
|
197
|
+
if (this.isDisposed) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.isDisposed = true;
|
|
202
|
+
const error = routerStoppedError();
|
|
157
203
|
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
for (const pendingWait of this.pendingAppWaits) {
|
|
205
|
+
pendingWait.reject(error);
|
|
160
206
|
}
|
|
207
|
+
}
|
|
161
208
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
209
|
+
waitForApp(applicationId?: string, timeout: number = DEFAULT_APP_TIMEOUT_MS): Promise<AppInfo> {
|
|
210
|
+
if (this.isDisposed) {
|
|
211
|
+
return Promise.reject(routerStoppedError());
|
|
212
|
+
}
|
|
165
213
|
|
|
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);
|
|
214
|
+
const app = applicationId === undefined ? this.getDefaultApp() : this.apps.get(applicationId);
|
|
173
215
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
resolve((event as AppRegisteredEvent).detail);
|
|
178
|
-
};
|
|
216
|
+
if (app) {
|
|
217
|
+
return Promise.resolve(app.info);
|
|
218
|
+
}
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
});
|
|
220
|
+
return this.waitForRegistration(applicationId, timeout);
|
|
182
221
|
}
|
|
183
222
|
|
|
184
|
-
async sendToApp<T>(
|
|
223
|
+
async sendToApp<T>(
|
|
224
|
+
applicationId: string | undefined,
|
|
225
|
+
method: string,
|
|
226
|
+
params?: RequestParams,
|
|
227
|
+
waitTimeout?: number,
|
|
228
|
+
): Promise<T> {
|
|
229
|
+
await this.waitForApp(applicationId, waitTimeout);
|
|
185
230
|
const app = this.resolveTargetApp(applicationId);
|
|
186
231
|
|
|
187
232
|
try {
|
|
188
|
-
return await app.connection.send<T>(method, params,
|
|
233
|
+
return await app.connection.send<T>(method, params, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
189
234
|
} catch (error) {
|
|
190
235
|
if (error instanceof McpError) {
|
|
191
236
|
throw this.toAppError(app, error);
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from "./transport.js";
|
|
10
10
|
|
|
11
11
|
class ConnectionRegistry extends EventTarget implements AppConnections {
|
|
12
|
-
private connections: Map<string, ProtocolConnection> = new Map();
|
|
13
12
|
private sockets: Map<string, Socket> = new Map();
|
|
14
13
|
|
|
15
14
|
onRequest: ConnectionRequestHandler = (_connection, request) => Promise.reject(methodNotFoundError(request.method));
|
|
@@ -17,14 +16,12 @@ class ConnectionRegistry extends EventTarget implements AppConnections {
|
|
|
17
16
|
register(socket: Socket): ProtocolConnection {
|
|
18
17
|
const connection = ProtocolConnection.fromSocket(socket, {
|
|
19
18
|
onClose: () => {
|
|
20
|
-
this.connections.delete(connection.id);
|
|
21
19
|
this.sockets.delete(connection.id);
|
|
22
20
|
this.dispatchEvent(connectionDisconnectionEvent(connection));
|
|
23
21
|
},
|
|
24
22
|
onError: (error) => this.dispatchEvent(connectionErrorEvent(error)),
|
|
25
23
|
});
|
|
26
24
|
|
|
27
|
-
this.connections.set(connection.id, connection);
|
|
28
25
|
this.sockets.set(connection.id, socket);
|
|
29
26
|
connection.fallbackRequestHandler = (request) => this.onRequest(connection, request);
|
|
30
27
|
|
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 = {
|
|
@@ -22,11 +20,7 @@ type SerializedProperty = {
|
|
|
22
20
|
note?: string;
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
type AppInfo =
|
|
26
|
-
applicationId: string;
|
|
27
|
-
pid: number;
|
|
28
|
-
projectRoot?: string;
|
|
29
|
-
};
|
|
23
|
+
type AppInfo = z.output<typeof RegisterParamsSchema>;
|
|
30
24
|
|
|
31
25
|
type ServerRequestParams<Method extends keyof typeof ServerRequestParamsSchemas> = z.infer<
|
|
32
26
|
(typeof ServerRequestParamsSchemas)[Method]
|
|
@@ -130,12 +124,6 @@ const ServerRequestParamsSchemas: {
|
|
|
130
124
|
"widget.screenshot": screenshotParams,
|
|
131
125
|
};
|
|
132
126
|
|
|
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
127
|
export {
|
|
140
128
|
DEFAULT_SUBTREE_DEPTH,
|
|
141
129
|
MAX_SUBTREE_WIDGETS,
|
|
@@ -148,7 +136,6 @@ export {
|
|
|
148
136
|
fireEventParams,
|
|
149
137
|
screenshotParams,
|
|
150
138
|
ServerRequestParamsSchemas,
|
|
151
|
-
DEFAULT_SOCKET_PATH,
|
|
152
139
|
type SerializedWidget,
|
|
153
140
|
type SerializedProperty,
|
|
154
141
|
type AppInfo,
|