@zooid/context-mcp 0.7.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/LICENSE +21 -0
- package/dist/bin.js +43 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-QQD76TLH.js +203 -0
- package/dist/chunk-QQD76TLH.js.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bin.ts +49 -0
- package/src/daemon-socket.test.ts +221 -0
- package/src/daemon-socket.ts +148 -0
- package/src/factory.test.ts +32 -0
- package/src/factory.ts +35 -0
- package/src/index.ts +6 -0
- package/src/integration.test.ts +138 -0
- package/src/mcp-server.test.ts +186 -0
- package/src/mcp-server.ts +92 -0
- package/src/spawn-registry.test.ts +58 -0
- package/src/spawn-registry.ts +25 -0
- package/src/types.ts +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zooid contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildContextMcpServer,
|
|
4
|
+
callDaemon
|
|
5
|
+
} from "./chunk-QQD76TLH.js";
|
|
6
|
+
|
|
7
|
+
// src/bin.ts
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
var spawnIdIdx = process.argv.indexOf("--spawn-id");
|
|
10
|
+
var spawnId = spawnIdIdx >= 0 ? process.argv[spawnIdIdx + 1] : void 0;
|
|
11
|
+
var sockPath = process.env.ZOOID_DAEMON_SOCK;
|
|
12
|
+
if (!spawnId || !sockPath) {
|
|
13
|
+
process.stderr.write("zooid-context-mcp: --spawn-id and ZOOID_DAEMON_SOCK are required\n");
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
process.stderr.write(
|
|
17
|
+
`zooid-context-mcp: starting (pid=${process.pid} spawnId=${spawnId} sock=${sockPath})
|
|
18
|
+
`
|
|
19
|
+
);
|
|
20
|
+
var remoteProvider = {
|
|
21
|
+
getRoomHistory: (_channelId, opts) => callDaemon(sockPath, {
|
|
22
|
+
spawnId,
|
|
23
|
+
method: "getRoomHistory",
|
|
24
|
+
params: opts ?? {}
|
|
25
|
+
}),
|
|
26
|
+
getRecentThreads: (_channelId, opts) => callDaemon(sockPath, {
|
|
27
|
+
spawnId,
|
|
28
|
+
method: "getRecentThreads",
|
|
29
|
+
params: opts ?? {}
|
|
30
|
+
}),
|
|
31
|
+
getThreadHistory: (_channelId, threadId, opts) => callDaemon(sockPath, {
|
|
32
|
+
spawnId,
|
|
33
|
+
method: "getThreadHistory",
|
|
34
|
+
params: { ...opts ?? {}, threadId }
|
|
35
|
+
}),
|
|
36
|
+
getChannelMembers: () => callDaemon(sockPath, { spawnId, method: "getChannelMembers", params: {} }),
|
|
37
|
+
getChannelInfo: () => callDaemon(sockPath, { spawnId, method: "getChannelInfo", params: {} })
|
|
38
|
+
};
|
|
39
|
+
var server = buildContextMcpServer({ resolve: async () => remoteProvider });
|
|
40
|
+
await server.connect(new StdioServerTransport());
|
|
41
|
+
process.stderr.write(`zooid-context-mcp: ready (spawnId=${spawnId})
|
|
42
|
+
`);
|
|
43
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { buildContextMcpServer } from './mcp-server.js'\nimport { callDaemon } from './daemon-socket.js'\nimport type { TransportContextProvider } from '@zooid/core'\n\nconst spawnIdIdx = process.argv.indexOf('--spawn-id')\nconst spawnId = spawnIdIdx >= 0 ? process.argv[spawnIdIdx + 1] : undefined\nconst sockPath = process.env.ZOOID_DAEMON_SOCK\nif (!spawnId || !sockPath) {\n process.stderr.write('zooid-context-mcp: --spawn-id and ZOOID_DAEMON_SOCK are required\\n')\n process.exit(2)\n}\nprocess.stderr.write(\n `zooid-context-mcp: starting (pid=${process.pid} spawnId=${spawnId} sock=${sockPath})\\n`,\n)\n\nconst remoteProvider: TransportContextProvider = {\n getRoomHistory: (_channelId, opts) =>\n callDaemon(sockPath, {\n spawnId,\n method: 'getRoomHistory',\n params: (opts ?? {}) as Record<string, unknown>,\n }) as Promise<Awaited<ReturnType<TransportContextProvider['getRoomHistory']>>>,\n getRecentThreads: (_channelId, opts) =>\n callDaemon(sockPath, {\n spawnId,\n method: 'getRecentThreads',\n params: (opts ?? {}) as Record<string, unknown>,\n }) as Promise<Awaited<ReturnType<TransportContextProvider['getRecentThreads']>>>,\n getThreadHistory: (_channelId, threadId, opts) =>\n callDaemon(sockPath, {\n spawnId,\n method: 'getThreadHistory',\n params: { ...(opts ?? {}), threadId } as Record<string, unknown>,\n }) as Promise<Awaited<ReturnType<TransportContextProvider['getThreadHistory']>>>,\n getChannelMembers: () =>\n callDaemon(sockPath, { spawnId, method: 'getChannelMembers', params: {} }) as Promise<\n Awaited<ReturnType<TransportContextProvider['getChannelMembers']>>\n >,\n getChannelInfo: () =>\n callDaemon(sockPath, { spawnId, method: 'getChannelInfo', params: {} }) as Promise<\n Awaited<ReturnType<TransportContextProvider['getChannelInfo']>>\n >,\n}\n\nconst server = buildContextMcpServer({ resolve: async () => remoteProvider })\nawait server.connect(new StdioServerTransport())\nprocess.stderr.write(`zooid-context-mcp: ready (spawnId=${spawnId})\\n`)\n"],"mappings":";;;;;;;AACA,SAAS,4BAA4B;AAKrC,IAAM,aAAa,QAAQ,KAAK,QAAQ,YAAY;AACpD,IAAM,UAAU,cAAc,IAAI,QAAQ,KAAK,aAAa,CAAC,IAAI;AACjE,IAAM,WAAW,QAAQ,IAAI;AAC7B,IAAI,CAAC,WAAW,CAAC,UAAU;AACzB,UAAQ,OAAO,MAAM,oEAAoE;AACzF,UAAQ,KAAK,CAAC;AAChB;AACA,QAAQ,OAAO;AAAA,EACb,oCAAoC,QAAQ,GAAG,YAAY,OAAO,SAAS,QAAQ;AAAA;AACrF;AAEA,IAAM,iBAA2C;AAAA,EAC/C,gBAAgB,CAAC,YAAY,SAC3B,WAAW,UAAU;AAAA,IACnB;AAAA,IACA,QAAQ;AAAA,IACR,QAAS,QAAQ,CAAC;AAAA,EACpB,CAAC;AAAA,EACH,kBAAkB,CAAC,YAAY,SAC7B,WAAW,UAAU;AAAA,IACnB;AAAA,IACA,QAAQ;AAAA,IACR,QAAS,QAAQ,CAAC;AAAA,EACpB,CAAC;AAAA,EACH,kBAAkB,CAAC,YAAY,UAAU,SACvC,WAAW,UAAU;AAAA,IACnB;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ,EAAE,GAAI,QAAQ,CAAC,GAAI,SAAS;AAAA,EACtC,CAAC;AAAA,EACH,mBAAmB,MACjB,WAAW,UAAU,EAAE,SAAS,QAAQ,qBAAqB,QAAQ,CAAC,EAAE,CAAC;AAAA,EAG3E,gBAAgB,MACd,WAAW,UAAU,EAAE,SAAS,QAAQ,kBAAkB,QAAQ,CAAC,EAAE,CAAC;AAG1E;AAEA,IAAM,SAAS,sBAAsB,EAAE,SAAS,YAAY,eAAe,CAAC;AAC5E,MAAM,OAAO,QAAQ,IAAI,qBAAqB,CAAC;AAC/C,QAAQ,OAAO,MAAM,qCAAqC,OAAO;AAAA,CAAK;","names":[]}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/mcp-server.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
var MAX_LIMIT = 200;
|
|
5
|
+
var DEFAULT_LIMIT = 50;
|
|
6
|
+
function buildContextMcpServer(opts) {
|
|
7
|
+
const server = new McpServer({ name: "zooid-context", version: "0.0.1" });
|
|
8
|
+
server.tool(
|
|
9
|
+
"zooid_get_history",
|
|
10
|
+
"Read every message in the current room chronologically \u2014 top-level messages and all thread replies. Each message has an optional `thread_id` so the agent can group by thread. For a scan-the-room overview without reply noise, use `zooid_get_recent_threads` instead. Supports `limit` + `before` pagination.",
|
|
11
|
+
{
|
|
12
|
+
limit: z.number().int().positive().optional(),
|
|
13
|
+
before: z.string().optional()
|
|
14
|
+
},
|
|
15
|
+
async ({ limit, before }) => {
|
|
16
|
+
const provider = await opts.resolve();
|
|
17
|
+
const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
18
|
+
const page = await provider.getRoomHistory("", { limit: clamped, before });
|
|
19
|
+
return { content: [{ type: "text", text: JSON.stringify(page) }] };
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
server.tool(
|
|
23
|
+
"zooid_get_recent_threads",
|
|
24
|
+
"Scan-the-room overview: top-level messages and thread roots in the current room, newest first. Each entry has `reply_count` and `last_activity_at` so the agent can spot active conversations. Drill into a thread with `zooid_get_thread_history(thread_id)` where `thread_id` is the entry's `id`.",
|
|
25
|
+
{
|
|
26
|
+
limit: z.number().int().positive().optional(),
|
|
27
|
+
before: z.string().optional()
|
|
28
|
+
},
|
|
29
|
+
async ({ limit, before }) => {
|
|
30
|
+
const provider = await opts.resolve();
|
|
31
|
+
const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
32
|
+
const page = await provider.getRecentThreads("", { limit: clamped, before });
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(page) }] };
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
server.tool(
|
|
37
|
+
"zooid_get_thread_history",
|
|
38
|
+
"Drill into a specific thread: the root message followed by all replies in chronological order. Pass the `thread_id` from a `zooid_get_recent_threads` entry or a `Message.thread_id` from `zooid_get_history`.",
|
|
39
|
+
{
|
|
40
|
+
thread_id: z.string(),
|
|
41
|
+
limit: z.number().int().positive().optional(),
|
|
42
|
+
before: z.string().optional()
|
|
43
|
+
},
|
|
44
|
+
async ({ thread_id, limit, before }) => {
|
|
45
|
+
const provider = await opts.resolve();
|
|
46
|
+
const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
47
|
+
const page = await provider.getThreadHistory("", thread_id, {
|
|
48
|
+
limit: clamped,
|
|
49
|
+
before
|
|
50
|
+
});
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(page) }] };
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
server.tool(
|
|
55
|
+
"zooid_get_members",
|
|
56
|
+
"List the humans and agents in the current room.",
|
|
57
|
+
{},
|
|
58
|
+
async () => {
|
|
59
|
+
const provider = await opts.resolve();
|
|
60
|
+
const members = await provider.getChannelMembers("");
|
|
61
|
+
return { content: [{ type: "text", text: JSON.stringify({ members }) }] };
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
server.tool(
|
|
65
|
+
"zooid_get_channel_info",
|
|
66
|
+
"Describe the current room: id, display name, transport kind.",
|
|
67
|
+
{},
|
|
68
|
+
async () => {
|
|
69
|
+
const provider = await opts.resolve();
|
|
70
|
+
const info = await provider.getChannelInfo("");
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(info) }] };
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
return server;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/daemon-socket.ts
|
|
78
|
+
import { createServer, createConnection } from "net";
|
|
79
|
+
import { unlink } from "fs/promises";
|
|
80
|
+
async function startDaemonSocketServer(opts) {
|
|
81
|
+
await unlink(opts.sockPath).catch(() => {
|
|
82
|
+
});
|
|
83
|
+
const server = createServer((socket) => {
|
|
84
|
+
let buf = "";
|
|
85
|
+
socket.setEncoding("utf8");
|
|
86
|
+
socket.on("data", async (chunk) => {
|
|
87
|
+
buf += chunk;
|
|
88
|
+
let idx;
|
|
89
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
90
|
+
const line = buf.slice(0, idx);
|
|
91
|
+
buf = buf.slice(idx + 1);
|
|
92
|
+
if (!line) continue;
|
|
93
|
+
await handleLine(line, socket, opts.registry);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
socket.on("error", () => {
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
server.once("error", reject);
|
|
101
|
+
server.listen(opts.sockPath, () => {
|
|
102
|
+
server.removeListener("error", reject);
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
close: () => new Promise((resolve) => {
|
|
108
|
+
server.close(() => resolve());
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function handleLine(line, socket, registry) {
|
|
113
|
+
let req;
|
|
114
|
+
try {
|
|
115
|
+
req = JSON.parse(line);
|
|
116
|
+
} catch {
|
|
117
|
+
socket.write(JSON.stringify({ ok: false, error: "invalid json" }) + "\n");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const binding = registry.get(req.spawnId);
|
|
121
|
+
if (!binding) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`[context-mcp] daemon: unknown spawn-id ${req.spawnId} for method=${req.method}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
socket.write(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
ok: false,
|
|
129
|
+
error: `unknown spawn-id: ${req.spawnId}`
|
|
130
|
+
}) + "\n"
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
`[context-mcp] daemon: ${req.method} spawn=${req.spawnId.slice(0, 8)} agent=${binding.agentName}
|
|
136
|
+
`
|
|
137
|
+
);
|
|
138
|
+
try {
|
|
139
|
+
let result;
|
|
140
|
+
const channelId = binding.threadRef.channelId;
|
|
141
|
+
if (req.method === "getRoomHistory") {
|
|
142
|
+
result = await binding.provider.getRoomHistory(channelId, req.params);
|
|
143
|
+
} else if (req.method === "getRecentThreads") {
|
|
144
|
+
result = await binding.provider.getRecentThreads(channelId, req.params);
|
|
145
|
+
} else if (req.method === "getThreadHistory") {
|
|
146
|
+
const threadId = String(req.params.threadId ?? "");
|
|
147
|
+
result = await binding.provider.getThreadHistory(channelId, threadId, req.params);
|
|
148
|
+
} else if (req.method === "getChannelMembers") {
|
|
149
|
+
result = await binding.provider.getChannelMembers(channelId);
|
|
150
|
+
} else if (req.method === "getChannelInfo") {
|
|
151
|
+
result = await binding.provider.getChannelInfo(channelId);
|
|
152
|
+
} else {
|
|
153
|
+
socket.write(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
ok: false,
|
|
156
|
+
error: `unknown method: ${req.method}`
|
|
157
|
+
}) + "\n"
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
socket.write(JSON.stringify({ ok: true, result }) + "\n");
|
|
162
|
+
} catch (err) {
|
|
163
|
+
socket.write(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
ok: false,
|
|
166
|
+
error: String(err instanceof Error ? err.message : err)
|
|
167
|
+
}) + "\n"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function callDaemon(sockPath, req) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const socket = createConnection(sockPath);
|
|
174
|
+
let buf = "";
|
|
175
|
+
socket.setEncoding("utf8");
|
|
176
|
+
socket.on("error", reject);
|
|
177
|
+
socket.on("data", (chunk) => {
|
|
178
|
+
buf += chunk;
|
|
179
|
+
const idx = buf.indexOf("\n");
|
|
180
|
+
if (idx < 0) return;
|
|
181
|
+
const line = buf.slice(0, idx);
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(line);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
socket.end();
|
|
187
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
socket.end();
|
|
191
|
+
if (parsed.ok) resolve(parsed.result);
|
|
192
|
+
else reject(new Error(parsed.error));
|
|
193
|
+
});
|
|
194
|
+
socket.write(JSON.stringify(req) + "\n");
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export {
|
|
199
|
+
buildContextMcpServer,
|
|
200
|
+
startDaemonSocketServer,
|
|
201
|
+
callDaemon
|
|
202
|
+
};
|
|
203
|
+
//# sourceMappingURL=chunk-QQD76TLH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-server.ts","../src/daemon-socket.ts"],"sourcesContent":["import { z } from 'zod'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { TransportContextProvider } from '@zooid/core'\n\nconst MAX_LIMIT = 200\nconst DEFAULT_LIMIT = 50\n\nexport interface BuildContextMcpServerOpts {\n /**\n * Resolves the provider for the current spawn. In production this calls\n * back to the daemon over the Unix socket; in unit tests it returns a\n * fake provider directly.\n */\n resolve: () => Promise<TransportContextProvider>\n}\n\nexport function buildContextMcpServer(opts: BuildContextMcpServerOpts): McpServer {\n const server = new McpServer({ name: 'zooid-context', version: '0.0.1' })\n\n server.tool(\n 'zooid_get_history',\n \"Read every message in the current room chronologically — top-level messages and all thread replies. Each message has an optional `thread_id` so the agent can group by thread. For a scan-the-room overview without reply noise, use `zooid_get_recent_threads` instead. Supports `limit` + `before` pagination.\",\n {\n limit: z.number().int().positive().optional(),\n before: z.string().optional(),\n },\n async ({ limit, before }) => {\n const provider = await opts.resolve()\n const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT)\n const page = await provider.getRoomHistory('', { limit: clamped, before })\n return { content: [{ type: 'text', text: JSON.stringify(page) }] }\n },\n )\n\n server.tool(\n 'zooid_get_recent_threads',\n \"Scan-the-room overview: top-level messages and thread roots in the current room, newest first. Each entry has `reply_count` and `last_activity_at` so the agent can spot active conversations. Drill into a thread with `zooid_get_thread_history(thread_id)` where `thread_id` is the entry's `id`.\",\n {\n limit: z.number().int().positive().optional(),\n before: z.string().optional(),\n },\n async ({ limit, before }) => {\n const provider = await opts.resolve()\n const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT)\n const page = await provider.getRecentThreads('', { limit: clamped, before })\n return { content: [{ type: 'text', text: JSON.stringify(page) }] }\n },\n )\n\n server.tool(\n 'zooid_get_thread_history',\n \"Drill into a specific thread: the root message followed by all replies in chronological order. Pass the `thread_id` from a `zooid_get_recent_threads` entry or a `Message.thread_id` from `zooid_get_history`.\",\n {\n thread_id: z.string(),\n limit: z.number().int().positive().optional(),\n before: z.string().optional(),\n },\n async ({ thread_id, limit, before }) => {\n const provider = await opts.resolve()\n const clamped = Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT)\n const page = await provider.getThreadHistory('', thread_id, {\n limit: clamped,\n before,\n })\n return { content: [{ type: 'text', text: JSON.stringify(page) }] }\n },\n )\n\n server.tool(\n 'zooid_get_members',\n 'List the humans and agents in the current room.',\n {},\n async () => {\n const provider = await opts.resolve()\n const members = await provider.getChannelMembers('')\n return { content: [{ type: 'text', text: JSON.stringify({ members }) }] }\n },\n )\n\n server.tool(\n 'zooid_get_channel_info',\n 'Describe the current room: id, display name, transport kind.',\n {},\n async () => {\n const provider = await opts.resolve()\n const info = await provider.getChannelInfo('')\n return { content: [{ type: 'text', text: JSON.stringify(info) }] }\n },\n )\n\n return server\n}\n","import { createServer, createConnection, type Server, type Socket } from 'node:net'\nimport { unlink } from 'node:fs/promises'\nimport type { SpawnRegistry } from './spawn-registry.js'\n\ninterface DaemonRequest {\n spawnId: string\n method:\n | 'getRoomHistory'\n | 'getRecentThreads'\n | 'getThreadHistory'\n | 'getChannelMembers'\n | 'getChannelInfo'\n params: Record<string, unknown>\n}\n\ninterface DaemonResponse {\n ok: true\n result: unknown\n}\n\ninterface DaemonError {\n ok: false\n error: string\n}\n\nexport interface DaemonSocketHandle {\n close(): Promise<void>\n}\n\nexport async function startDaemonSocketServer(opts: {\n sockPath: string\n registry: SpawnRegistry\n}): Promise<DaemonSocketHandle> {\n await unlink(opts.sockPath).catch(() => {})\n const server: Server = createServer((socket: Socket) => {\n let buf = ''\n socket.setEncoding('utf8')\n socket.on('data', async (chunk) => {\n buf += chunk\n let idx: number\n while ((idx = buf.indexOf('\\n')) >= 0) {\n const line = buf.slice(0, idx)\n buf = buf.slice(idx + 1)\n if (!line) continue\n await handleLine(line, socket, opts.registry)\n }\n })\n socket.on('error', () => {})\n })\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(opts.sockPath, () => {\n server.removeListener('error', reject)\n resolve()\n })\n })\n return {\n close: () =>\n new Promise<void>((resolve) => {\n server.close(() => resolve())\n }),\n }\n}\n\nasync function handleLine(line: string, socket: Socket, registry: SpawnRegistry) {\n let req: DaemonRequest\n try {\n req = JSON.parse(line) as DaemonRequest\n } catch {\n socket.write(JSON.stringify({ ok: false, error: 'invalid json' } satisfies DaemonError) + '\\n')\n return\n }\n const binding = registry.get(req.spawnId)\n if (!binding) {\n process.stderr.write(\n `[context-mcp] daemon: unknown spawn-id ${req.spawnId} for method=${req.method}\\n`,\n )\n socket.write(\n JSON.stringify({\n ok: false,\n error: `unknown spawn-id: ${req.spawnId}`,\n } satisfies DaemonError) + '\\n',\n )\n return\n }\n process.stderr.write(\n `[context-mcp] daemon: ${req.method} spawn=${req.spawnId.slice(0, 8)} agent=${binding.agentName}\\n`,\n )\n try {\n let result: unknown\n const channelId = binding.threadRef.channelId\n if (req.method === 'getRoomHistory') {\n result = await binding.provider.getRoomHistory(channelId, req.params)\n } else if (req.method === 'getRecentThreads') {\n result = await binding.provider.getRecentThreads(channelId, req.params)\n } else if (req.method === 'getThreadHistory') {\n const threadId = String(req.params.threadId ?? '')\n result = await binding.provider.getThreadHistory(channelId, threadId, req.params)\n } else if (req.method === 'getChannelMembers') {\n result = await binding.provider.getChannelMembers(channelId)\n } else if (req.method === 'getChannelInfo') {\n result = await binding.provider.getChannelInfo(channelId)\n } else {\n socket.write(\n JSON.stringify({\n ok: false,\n error: `unknown method: ${(req as { method: string }).method}`,\n } satisfies DaemonError) + '\\n',\n )\n return\n }\n socket.write(JSON.stringify({ ok: true, result } satisfies DaemonResponse) + '\\n')\n } catch (err) {\n socket.write(\n JSON.stringify({\n ok: false,\n error: String(err instanceof Error ? err.message : err),\n } satisfies DaemonError) + '\\n',\n )\n }\n}\n\nexport async function callDaemon(sockPath: string, req: DaemonRequest): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const socket = createConnection(sockPath)\n let buf = ''\n socket.setEncoding('utf8')\n socket.on('error', reject)\n socket.on('data', (chunk) => {\n buf += chunk\n const idx = buf.indexOf('\\n')\n if (idx < 0) return\n const line = buf.slice(0, idx)\n let parsed: DaemonResponse | DaemonError\n try {\n parsed = JSON.parse(line) as DaemonResponse | DaemonError\n } catch (e) {\n socket.end()\n reject(e instanceof Error ? e : new Error(String(e)))\n return\n }\n socket.end()\n if (parsed.ok) resolve(parsed.result)\n else reject(new Error(parsed.error))\n })\n socket.write(JSON.stringify(req) + '\\n')\n })\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildContextMcpServer,
|
|
3
|
+
callDaemon,
|
|
4
|
+
startDaemonSocketServer
|
|
5
|
+
} from "./chunk-QQD76TLH.js";
|
|
6
|
+
|
|
7
|
+
// src/spawn-registry.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
var SpawnRegistry = class {
|
|
10
|
+
bindings = /* @__PURE__ */ new Map();
|
|
11
|
+
register(input) {
|
|
12
|
+
const spawnId = randomUUID();
|
|
13
|
+
this.bindings.set(spawnId, { spawnId, ...input });
|
|
14
|
+
return spawnId;
|
|
15
|
+
}
|
|
16
|
+
get(spawnId) {
|
|
17
|
+
return this.bindings.get(spawnId);
|
|
18
|
+
}
|
|
19
|
+
release(spawnId) {
|
|
20
|
+
this.bindings.delete(spawnId);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/factory.ts
|
|
25
|
+
import { createRequire } from "module";
|
|
26
|
+
function resolveDefaultBin() {
|
|
27
|
+
const req = createRequire(import.meta.url);
|
|
28
|
+
return req.resolve("@zooid/context-mcp/bin");
|
|
29
|
+
}
|
|
30
|
+
var cachedDefaultBin = null;
|
|
31
|
+
function getDefaultBin() {
|
|
32
|
+
if (!cachedDefaultBin) cachedDefaultBin = resolveDefaultBin();
|
|
33
|
+
return cachedDefaultBin;
|
|
34
|
+
}
|
|
35
|
+
function buildContextServerSpec(opts) {
|
|
36
|
+
return {
|
|
37
|
+
name: "zooid-context",
|
|
38
|
+
command: process.execPath,
|
|
39
|
+
args: [opts.binPath ?? getDefaultBin(), "--spawn-id", opts.spawnId],
|
|
40
|
+
env: [{ name: "ZOOID_DAEMON_SOCK", value: opts.sockPath }]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
SpawnRegistry,
|
|
45
|
+
buildContextMcpServer,
|
|
46
|
+
buildContextServerSpec,
|
|
47
|
+
callDaemon,
|
|
48
|
+
startDaemonSocketServer
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spawn-registry.ts","../src/factory.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type { SpawnBinding } from './types.js'\nimport type { TransportContextProvider, ThreadRef } from '@zooid/core'\n\nexport class SpawnRegistry {\n private readonly bindings = new Map<string, SpawnBinding>()\n\n register(input: {\n agentName: string\n threadRef: ThreadRef\n provider: TransportContextProvider\n }): string {\n const spawnId = randomUUID()\n this.bindings.set(spawnId, { spawnId, ...input })\n return spawnId\n }\n\n get(spawnId: string): SpawnBinding | undefined {\n return this.bindings.get(spawnId)\n }\n\n release(spawnId: string): void {\n this.bindings.delete(spawnId)\n }\n}\n","import { createRequire } from 'node:module'\nimport type { ZooidContextServerSpec } from './types.js'\n\n/**\n * Resolve `dist/bin.js` via Node's runtime module resolver rather than\n * `import.meta.url`. tsup bundles this file into the CLI's own chunk, so\n * `import.meta.url` at runtime points to the CLI bundle — not this\n * package's dist. `createRequire` walks the runtime `node_modules` tree\n * instead and finds the package wherever it actually lives.\n */\nfunction resolveDefaultBin(): string {\n const req = createRequire(import.meta.url)\n // Resolve via the dedicated `./bin` export → `./dist/bin.js`. Avoids the\n // exports-restriction surprise of importing `./package.json`.\n return req.resolve('@zooid/context-mcp/bin')\n}\n\nlet cachedDefaultBin: string | null = null\nfunction getDefaultBin(): string {\n if (!cachedDefaultBin) cachedDefaultBin = resolveDefaultBin()\n return cachedDefaultBin\n}\n\nexport function buildContextServerSpec(opts: {\n spawnId: string\n sockPath: string\n binPath?: string\n}): ZooidContextServerSpec {\n return {\n name: 'zooid-context',\n command: process.execPath,\n args: [opts.binPath ?? getDefaultBin(), '--spawn-id', opts.spawnId],\n env: [{ name: 'ZOOID_DAEMON_SOCK', value: opts.sockPath }],\n }\n}\n"],"mappings":";;;;;;;AAAA,SAAS,kBAAkB;AAIpB,IAAM,gBAAN,MAAoB;AAAA,EACR,WAAW,oBAAI,IAA0B;AAAA,EAE1D,SAAS,OAIE;AACT,UAAM,UAAU,WAAW;AAC3B,SAAK,SAAS,IAAI,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC;AAChD,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,SAA2C;AAC7C,WAAO,KAAK,SAAS,IAAI,OAAO;AAAA,EAClC;AAAA,EAEA,QAAQ,SAAuB;AAC7B,SAAK,SAAS,OAAO,OAAO;AAAA,EAC9B;AACF;;;ACxBA,SAAS,qBAAqB;AAU9B,SAAS,oBAA4B;AACnC,QAAM,MAAM,cAAc,YAAY,GAAG;AAGzC,SAAO,IAAI,QAAQ,wBAAwB;AAC7C;AAEA,IAAI,mBAAkC;AACtC,SAAS,gBAAwB;AAC/B,MAAI,CAAC,iBAAkB,oBAAmB,kBAAkB;AAC5D,SAAO;AACT;AAEO,SAAS,uBAAuB,MAIZ;AACzB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,QAAQ;AAAA,IACjB,MAAM,CAAC,KAAK,WAAW,cAAc,GAAG,cAAc,KAAK,OAAO;AAAA,IAClE,KAAK,CAAC,EAAE,MAAM,qBAAqB,OAAO,KAAK,SAAS,CAAC;AAAA,EAC3D;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zooid/context-mcp",
|
|
3
|
+
"version": "0.7.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"zooid-context-mcp": "./dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"import": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"./bin": "./dist/bin.js",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
33
|
+
"zod": "^3.23.0",
|
|
34
|
+
"@zooid/core": "^0.7.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"typescript": "^5.5.0",
|
|
41
|
+
"vitest": "^3.2.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { buildContextMcpServer } from './mcp-server.js'
|
|
4
|
+
import { callDaemon } from './daemon-socket.js'
|
|
5
|
+
import type { TransportContextProvider } from '@zooid/core'
|
|
6
|
+
|
|
7
|
+
const spawnIdIdx = process.argv.indexOf('--spawn-id')
|
|
8
|
+
const spawnId = spawnIdIdx >= 0 ? process.argv[spawnIdIdx + 1] : undefined
|
|
9
|
+
const sockPath = process.env.ZOOID_DAEMON_SOCK
|
|
10
|
+
if (!spawnId || !sockPath) {
|
|
11
|
+
process.stderr.write('zooid-context-mcp: --spawn-id and ZOOID_DAEMON_SOCK are required\n')
|
|
12
|
+
process.exit(2)
|
|
13
|
+
}
|
|
14
|
+
process.stderr.write(
|
|
15
|
+
`zooid-context-mcp: starting (pid=${process.pid} spawnId=${spawnId} sock=${sockPath})\n`,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const remoteProvider: TransportContextProvider = {
|
|
19
|
+
getRoomHistory: (_channelId, opts) =>
|
|
20
|
+
callDaemon(sockPath, {
|
|
21
|
+
spawnId,
|
|
22
|
+
method: 'getRoomHistory',
|
|
23
|
+
params: (opts ?? {}) as Record<string, unknown>,
|
|
24
|
+
}) as Promise<Awaited<ReturnType<TransportContextProvider['getRoomHistory']>>>,
|
|
25
|
+
getRecentThreads: (_channelId, opts) =>
|
|
26
|
+
callDaemon(sockPath, {
|
|
27
|
+
spawnId,
|
|
28
|
+
method: 'getRecentThreads',
|
|
29
|
+
params: (opts ?? {}) as Record<string, unknown>,
|
|
30
|
+
}) as Promise<Awaited<ReturnType<TransportContextProvider['getRecentThreads']>>>,
|
|
31
|
+
getThreadHistory: (_channelId, threadId, opts) =>
|
|
32
|
+
callDaemon(sockPath, {
|
|
33
|
+
spawnId,
|
|
34
|
+
method: 'getThreadHistory',
|
|
35
|
+
params: { ...(opts ?? {}), threadId } as Record<string, unknown>,
|
|
36
|
+
}) as Promise<Awaited<ReturnType<TransportContextProvider['getThreadHistory']>>>,
|
|
37
|
+
getChannelMembers: () =>
|
|
38
|
+
callDaemon(sockPath, { spawnId, method: 'getChannelMembers', params: {} }) as Promise<
|
|
39
|
+
Awaited<ReturnType<TransportContextProvider['getChannelMembers']>>
|
|
40
|
+
>,
|
|
41
|
+
getChannelInfo: () =>
|
|
42
|
+
callDaemon(sockPath, { spawnId, method: 'getChannelInfo', params: {} }) as Promise<
|
|
43
|
+
Awaited<ReturnType<TransportContextProvider['getChannelInfo']>>
|
|
44
|
+
>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const server = buildContextMcpServer({ resolve: async () => remoteProvider })
|
|
48
|
+
await server.connect(new StdioServerTransport())
|
|
49
|
+
process.stderr.write(`zooid-context-mcp: ready (spawnId=${spawnId})\n`)
|