@zooid/context-mcp 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  buildContextMcpServer,
3
3
  callDaemon,
4
4
  startDaemonSocketServer
5
- } from "./chunk-ZY2GPHFX.js";
5
+ } from "./chunk-L5USYHTS.js";
6
6
 
7
7
  // src/spawn-registry.ts
8
8
  import { randomUUID } from "crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zooid/context-mcp",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "description": "Daemon-side stdio MCP server + per-spawn registry that exposes zooid_get_history / zooid_get_members / zooid_get_channel_info to ACP shims, backed by each transport's TransportContextProvider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.0.4",
33
33
  "zod": "^3.23.0",
34
- "@zooid/core": "^0.11.2"
34
+ "@zooid/core": "^0.13.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",
@@ -2,6 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { randomUUID } from 'node:crypto'
5
+ import { createConnection } from 'node:net'
5
6
  import { SpawnRegistry } from './spawn-registry.js'
6
7
  import { startDaemonSocketServer, callDaemon } from './daemon-socket.js'
7
8
  import type { TransportContextProvider } from '@zooid/core'
@@ -219,3 +220,29 @@ describe('daemon-socket', () => {
219
220
  }
220
221
  })
221
222
  })
223
+
224
+ describe('close()', () => {
225
+ it('does not hang on a connected client that never disconnects', async () => {
226
+ // The context-mcp servers spawned beside each agent outlive
227
+ // AcpClient.stop() (it SIGTERMs the agent and doesn't wait; these are its
228
+ // grandchildren). net.Server.close() waits for every open connection and,
229
+ // unlike http.Server, never drops idle ones — so before this was fixed,
230
+ // `zooid dev` hung on shutdown for as long as those processes lived.
231
+ const sockPath = join(tmpdir(), `zooid-close-${randomUUID()}.sock`)
232
+ const registry = new SpawnRegistry()
233
+ const handle = await startDaemonSocketServer({ sockPath, registry })
234
+
235
+ const client = createConnection(sockPath)
236
+ await new Promise<void>((resolve, reject) => {
237
+ client.once('connect', () => resolve())
238
+ client.once('error', reject)
239
+ })
240
+
241
+ const closed = await Promise.race([
242
+ handle.close().then(() => 'closed' as const),
243
+ new Promise<'hung'>((resolve) => setTimeout(() => resolve('hung'), 2000)),
244
+ ])
245
+ client.destroy()
246
+ expect(closed).toBe('closed')
247
+ })
248
+ })
@@ -32,7 +32,17 @@ export async function startDaemonSocketServer(opts: {
32
32
  registry: SpawnRegistry
33
33
  }): Promise<DaemonSocketHandle> {
34
34
  await unlink(opts.sockPath).catch(() => {})
35
+ // net.Server.close() waits for every open connection and — unlike
36
+ // http.Server since Node 19 — never destroys idle ones, and there is no
37
+ // closeAllConnections() for it. The clients here are context-mcp servers
38
+ // spawned beside each agent, which outlive AcpClient.stop() (it SIGTERMs
39
+ // the agent without waiting, and these are its grandchildren). So without
40
+ // tracking them, close() hangs until they happen to die — which is what
41
+ // made `zooid dev` take ages to stop.
42
+ const open = new Set<Socket>()
35
43
  const server: Server = createServer((socket: Socket) => {
44
+ open.add(socket)
45
+ socket.on('close', () => open.delete(socket))
36
46
  let buf = ''
37
47
  socket.setEncoding('utf8')
38
48
  socket.on('data', async (chunk) => {
@@ -58,6 +68,11 @@ export async function startDaemonSocketServer(opts: {
58
68
  close: () =>
59
69
  new Promise<void>((resolve) => {
60
70
  server.close(() => resolve())
71
+ // Shutdown is already tearing the agents down, so there is no
72
+ // in-flight request worth waiting for. Drop the connections that
73
+ // would otherwise keep close() pending forever.
74
+ for (const socket of open) socket.destroy()
75
+ open.clear()
61
76
  }),
62
77
  }
63
78
  }