@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.
Files changed (44) hide show
  1. package/README.md +6 -169
  2. package/dist/app-router.d.ts +7 -7
  3. package/dist/app-router.d.ts.map +1 -1
  4. package/dist/app-router.js +63 -32
  5. package/dist/app-router.js.map +1 -1
  6. package/dist/connection-registry.d.ts +0 -1
  7. package/dist/connection-registry.d.ts.map +1 -1
  8. package/dist/connection-registry.js +0 -3
  9. package/dist/connection-registry.js.map +1 -1
  10. package/dist/internal.d.ts +2 -1
  11. package/dist/internal.d.ts.map +1 -1
  12. package/dist/internal.js +2 -1
  13. package/dist/internal.js.map +1 -1
  14. package/dist/protocol/schemas.d.ts +2 -7
  15. package/dist/protocol/schemas.d.ts.map +1 -1
  16. package/dist/protocol/schemas.js +1 -7
  17. package/dist/protocol/schemas.js.map +1 -1
  18. package/dist/reference.d.ts.map +1 -1
  19. package/dist/reference.js +32 -18
  20. package/dist/reference.js.map +1 -1
  21. package/dist/server.d.ts.map +1 -1
  22. package/dist/server.js +110 -62
  23. package/dist/server.js.map +1 -1
  24. package/dist/socket-path.d.ts +11 -0
  25. package/dist/socket-path.d.ts.map +1 -0
  26. package/dist/socket-path.js +74 -0
  27. package/dist/socket-path.js.map +1 -0
  28. package/dist/socket-server.d.ts +3 -1
  29. package/dist/socket-server.d.ts.map +1 -1
  30. package/dist/socket-server.js +48 -32
  31. package/dist/socket-server.js.map +1 -1
  32. package/dist/tool-filter.d.ts.map +1 -1
  33. package/dist/tool-filter.js +1 -3
  34. package/dist/tool-filter.js.map +1 -1
  35. package/package.json +22 -8
  36. package/src/app-router.ts +84 -39
  37. package/src/connection-registry.ts +0 -3
  38. package/src/internal.ts +4 -1
  39. package/src/protocol/schemas.ts +1 -14
  40. package/src/reference.ts +48 -19
  41. package/src/server.ts +133 -64
  42. package/src/socket-path.ts +103 -0
  43. package/src/socket-server.ts +61 -37
  44. package/src/tool-filter.ts +1 -4
@@ -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 { DEFAULT_SOCKET_PATH } from "./protocol/schemas.js";
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 directoryPathError = (socketPath) => new Error(`The GTKX MCP socket path ${socketPath} is a directory, not a socket. ` +
114
- "Remove it, or point XDG_RUNTIME_DIR at a directory where GTKX can create its socket.");
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.isDirectory()) {
128
- return { kind: "directory" };
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 === "directory") {
142
- throw directoryPathError(socketPath);
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, socketPath = DEFAULT_SOCKET_PATH) {
173
+ constructor(registry, address = resolveMcpSocketAddress()) {
187
174
  this.registry = registry;
188
- this.socketPath = socketPath;
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
- this.boundInode = await publishSocket(privatePath, this.socketPath);
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, () => this.bind());
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"]}
@@ -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;AAwCtC,QAAA,MAAM,WAAW,GAAI,OAAO,IAAI,EAAE,EAAE,UAAU,WAAW,KAAG,IAAI,EAU/D,CAAC;AAEF,OAAO,EAAE,WAAW,EAAE,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"}
@@ -1,7 +1,5 @@
1
1
  const NEGATION_PREFIX = "!";
2
- const SPECIAL_CHARACTERS = /[.+?^${}()|[\]\\]/g;
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);
@@ -1 +1 @@
1
- {"version":3,"file":"tool-filter.js","sourceRoot":"","sources":["../src/tool-filter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpSettings } from \"@gtkx/config/internal\";\nimport type { Tool } from \"./tool.js\";\n\nconst NEGATION_PREFIX = \"!\";\nconst SPECIAL_CHARACTERS = /[.+?^${}()|[\\]\\\\]/g;\n\nconst escapeLiteral = (value: string): string => value.replaceAll(SPECIAL_CHARACTERS, String.raw`\\$&`);\n\nconst patternToRegExp = (pattern: string): RegExp =>\n new RegExp(`^${pattern.split(\"*\").map((part) => escapeLiteral(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"]}
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.1",
4
- "description": "MCP server exposing the GTK API reference and a running GTK4 app.",
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.1",
67
- "@gtkx/config": "2.0.0-beta.1",
68
- "@gtkx/utils": "2.0.0-beta.1",
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.4"
84
+ "zod": "^4.6.5"
71
85
  },
72
86
  "devDependencies": {
73
- "@gtkx/vitest": "2.0.0-beta.1",
74
- "vitest": "^4.1.11"
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
- constructor(connections: AppConnections, options: { requestTimeout?: number } = {}) {
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
- const appInfo: AppInfo = {
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(appInfo));
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(this.requestTimeout);
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
- waitForApp(timeout: number = AppRouter.defaultWaitTimeout): Promise<AppInfo> {
156
- const defaultApp = this.getDefaultApp();
196
+ dispose(): void {
197
+ if (this.isDisposed) {
198
+ return;
199
+ }
200
+
201
+ this.isDisposed = true;
202
+ const error = routerStoppedError();
157
203
 
158
- if (defaultApp) {
159
- return Promise.resolve(defaultApp.info);
204
+ for (const pendingWait of this.pendingAppWaits) {
205
+ pendingWait.reject(error);
160
206
  }
207
+ }
161
208
 
162
- return new Promise((resolve, reject) => {
163
- const timeoutId = setTimeout(() => {
164
- this.removeEventListener("appRegistered", onRegister);
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
- reject(
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
- const onRegister = (event: Event): void => {
175
- clearTimeout(timeoutId);
176
- this.removeEventListener("appRegistered", onRegister);
177
- resolve((event as AppRegisteredEvent).detail);
178
- };
216
+ if (app) {
217
+ return Promise.resolve(app.info);
218
+ }
179
219
 
180
- this.addEventListener("appRegistered", onRegister);
181
- });
220
+ return this.waitForRegistration(applicationId, timeout);
182
221
  }
183
222
 
184
- async sendToApp<T>(applicationId: string | undefined, method: string, params?: RequestParams): Promise<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, this.requestTimeout);
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";
@@ -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,