@poolzin/pool-bot 2026.4.32 → 2026.4.34

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.
@@ -0,0 +1,463 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Server for PoolBot.
3
+ *
4
+ * Exposes PoolBot sessions and conversations to MCP-compatible clients
5
+ * like Claude Desktop, Cursor, VS Code, etc.
6
+ *
7
+ * Supports both stdio and Streamable HTTP transports.
8
+ *
9
+ * @see https://modelcontextprotocol.io/
10
+ */
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
15
+ import { createServer } from "node:http";
16
+ import { parse as parseUrl } from "node:url";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { loadConfig } from "../config/config.js";
20
+ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
21
+ // ── Session Database Helpers ─────────────────────────────────────────
22
+ function getSessionStorePath(workspaceDir, agentId) {
23
+ return join(workspaceDir, "memory", agentId, "sessions.json");
24
+ }
25
+ function loadSessions(workspaceDir, agentId) {
26
+ const storePath = getSessionStorePath(workspaceDir, agentId);
27
+ if (!existsSync(storePath)) {
28
+ return [];
29
+ }
30
+ try {
31
+ const content = readFileSync(storePath, "utf-8");
32
+ const store = JSON.parse(content);
33
+ return Object.entries(store.sessions || {})
34
+ .map(([key, value]) => {
35
+ const session = value;
36
+ return {
37
+ sessionId: session.sessionId || key,
38
+ sessionKey: key,
39
+ createdAt: session.createdAt || 0,
40
+ updatedAt: session.updatedAt || 0,
41
+ messageCount: session.messageCount || 0,
42
+ model: session.model,
43
+ };
44
+ })
45
+ .sort((a, b) => b.updatedAt - a.updatedAt);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ function getSessionMessagesPath(workspaceDir, agentId, sessionKey) {
52
+ return join(workspaceDir, "memory", agentId, "sessions", `${sessionKey}.jsonl`);
53
+ }
54
+ function loadSessionMessages(workspaceDir, agentId, sessionKey) {
55
+ const messagesPath = getSessionMessagesPath(workspaceDir, agentId, sessionKey);
56
+ if (!existsSync(messagesPath)) {
57
+ return [];
58
+ }
59
+ const messages = [];
60
+ const content = readFileSync(messagesPath, "utf-8");
61
+ const lines = content.trim().split("\n").filter(Boolean);
62
+ for (const line of lines) {
63
+ try {
64
+ const entry = JSON.parse(line);
65
+ if (entry.role === "user" || entry.role === "assistant" || entry.role === "system") {
66
+ messages.push({
67
+ role: entry.role,
68
+ content: entry.content || entry.message || "",
69
+ timestamp: entry.timestamp || entry.createdAt || 0,
70
+ toolCalls: entry.toolCalls || entry.tools,
71
+ });
72
+ }
73
+ }
74
+ catch {
75
+ continue;
76
+ }
77
+ }
78
+ return messages;
79
+ }
80
+ function searchSessions(workspaceDir, agentId, query, limit = 10) {
81
+ const sessions = loadSessions(workspaceDir, agentId);
82
+ const results = [];
83
+ const queryLower = query.toLowerCase();
84
+ for (const session of sessions.slice(0, 50)) { // Limit search to recent 50 sessions
85
+ const messages = loadSessionMessages(workspaceDir, agentId, session.sessionKey);
86
+ const matches = [];
87
+ for (const msg of messages) {
88
+ if (msg.content.toLowerCase().includes(queryLower)) {
89
+ // Extract context around the match
90
+ const idx = msg.content.toLowerCase().indexOf(queryLower);
91
+ const start = Math.max(0, idx - 50);
92
+ const end = Math.min(msg.content.length, idx + query.length + 50);
93
+ const excerpt = msg.content.substring(start, end).replace(/\n/g, " ").trim();
94
+ matches.push(`[${msg.role}] ${excerpt}...`);
95
+ if (matches.length >= 3)
96
+ break; // Max 3 matches per session
97
+ }
98
+ }
99
+ if (matches.length > 0) {
100
+ results.push({ session, matches });
101
+ }
102
+ if (results.length >= limit)
103
+ break;
104
+ }
105
+ return results;
106
+ }
107
+ // ── MCP Server Implementation ────────────────────────────────────────
108
+ export class PoolBotMCPServer {
109
+ server;
110
+ options;
111
+ workspaceDir;
112
+ agentId;
113
+ constructor(options = {}) {
114
+ this.options = {
115
+ httpPort: 3000,
116
+ readOnly: false,
117
+ ...options,
118
+ };
119
+ const config = loadConfig();
120
+ this.agentId = options.agentId || resolveDefaultAgentId(config);
121
+ this.workspaceDir = options.workspaceDir || resolveAgentWorkspaceDir(config, this.agentId);
122
+ this.server = new Server({
123
+ name: "poolbot-mcp",
124
+ version: "1.0.0",
125
+ }, {
126
+ capabilities: {
127
+ tools: {},
128
+ resources: {},
129
+ },
130
+ });
131
+ this.setupHandlers();
132
+ }
133
+ setupHandlers() {
134
+ // List available tools
135
+ this.server.setRequestHandler(ListToolsRequestSchema, () => {
136
+ const tools = [
137
+ {
138
+ name: "list_sessions",
139
+ description: "List all PoolBot sessions with metadata",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ limit: {
144
+ type: "number",
145
+ description: "Maximum number of sessions to return (default: 20)",
146
+ default: 20,
147
+ },
148
+ },
149
+ },
150
+ },
151
+ {
152
+ name: "get_session",
153
+ description: "Get details of a specific PoolBot session",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ sessionKey: {
158
+ type: "string",
159
+ description: "Session key or ID",
160
+ },
161
+ },
162
+ required: ["sessionKey"],
163
+ },
164
+ },
165
+ {
166
+ name: "get_messages",
167
+ description: "Get messages from a PoolBot session",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ sessionKey: {
172
+ type: "string",
173
+ description: "Session key or ID",
174
+ },
175
+ limit: {
176
+ type: "number",
177
+ description: "Maximum messages to return (default: 50)",
178
+ default: 50,
179
+ },
180
+ role: {
181
+ type: "string",
182
+ description: "Filter by role: user, assistant, or system",
183
+ enum: ["user", "assistant", "system"],
184
+ },
185
+ },
186
+ required: ["sessionKey"],
187
+ },
188
+ },
189
+ {
190
+ name: "search_sessions",
191
+ description: "Search across all PoolBot sessions for text",
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ query: {
196
+ type: "string",
197
+ description: "Search query",
198
+ },
199
+ limit: {
200
+ type: "number",
201
+ description: "Maximum results to return (default: 10)",
202
+ default: 10,
203
+ },
204
+ },
205
+ required: ["query"],
206
+ },
207
+ },
208
+ {
209
+ name: "get_session_stats",
210
+ description: "Get statistics about PoolBot sessions",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {},
214
+ },
215
+ },
216
+ ];
217
+ if (!this.options.readOnly) {
218
+ tools.push({
219
+ name: "send_message",
220
+ description: "Send a message to a PoolBot session",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ sessionKey: { type: "string", description: "Session key or ID" },
225
+ message: { type: "string", description: "Message to send" },
226
+ },
227
+ required: ["sessionKey", "message"],
228
+ },
229
+ });
230
+ }
231
+ return { tools };
232
+ });
233
+ // List available resources
234
+ this.server.setRequestHandler(ListResourcesRequestSchema, () => {
235
+ const sessions = loadSessions(this.workspaceDir, this.agentId);
236
+ const resources = sessions.slice(0, 100).map((session) => ({
237
+ uri: `poolbot://session/${session.sessionKey}`,
238
+ name: `Session ${session.sessionKey.substring(0, 8)}...`,
239
+ description: `${session.messageCount} messages, last updated ${new Date(session.updatedAt).toISOString()}`,
240
+ mimeType: "application/json",
241
+ }));
242
+ return { resources };
243
+ });
244
+ // Read resource content
245
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
246
+ const uri = request.params.uri;
247
+ const parsed = parseUrl(uri);
248
+ if (parsed.protocol === "poolbot:" && parsed.pathname?.startsWith("/session/")) {
249
+ const sessionKey = parsed.pathname.replace("/session/", "");
250
+ const messages = loadSessionMessages(this.workspaceDir, this.agentId, sessionKey);
251
+ return {
252
+ contents: [
253
+ {
254
+ uri,
255
+ mimeType: "application/json",
256
+ text: JSON.stringify(messages, null, 2),
257
+ },
258
+ ],
259
+ };
260
+ }
261
+ throw new Error(`Unknown resource: ${uri}`);
262
+ });
263
+ // Handle tool calls
264
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
265
+ const { name, arguments: args } = request.params;
266
+ try {
267
+ switch (name) {
268
+ case "list_sessions": {
269
+ const limit = args?.limit || 20;
270
+ const sessions = loadSessions(this.workspaceDir, this.agentId).slice(0, limit);
271
+ return {
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: JSON.stringify(sessions, null, 2),
276
+ },
277
+ ],
278
+ };
279
+ }
280
+ case "get_session": {
281
+ const sessionKey = args?.sessionKey;
282
+ if (!sessionKey) {
283
+ throw new Error("sessionKey is required");
284
+ }
285
+ const sessions = loadSessions(this.workspaceDir, this.agentId);
286
+ const session = sessions.find((s) => s.sessionKey === sessionKey || s.sessionId === sessionKey);
287
+ if (!session) {
288
+ throw new Error(`Session not found: ${sessionKey}`);
289
+ }
290
+ return {
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: JSON.stringify(session, null, 2),
295
+ },
296
+ ],
297
+ };
298
+ }
299
+ case "get_messages": {
300
+ const sessionKey = args?.sessionKey;
301
+ const limit = args?.limit || 50;
302
+ const role = args?.role;
303
+ if (!sessionKey) {
304
+ throw new Error("sessionKey is required");
305
+ }
306
+ let messages = loadSessionMessages(this.workspaceDir, this.agentId, sessionKey);
307
+ if (role) {
308
+ messages = messages.filter((m) => m.role === role);
309
+ }
310
+ messages = messages.slice(-limit);
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: JSON.stringify(messages, null, 2),
316
+ },
317
+ ],
318
+ };
319
+ }
320
+ case "search_sessions": {
321
+ const query = args?.query;
322
+ const limit = args?.limit || 10;
323
+ if (!query) {
324
+ throw new Error("query is required");
325
+ }
326
+ const results = searchSessions(this.workspaceDir, this.agentId, query, limit);
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: JSON.stringify(results, null, 2),
332
+ },
333
+ ],
334
+ };
335
+ }
336
+ case "get_session_stats": {
337
+ const sessions = loadSessions(this.workspaceDir, this.agentId);
338
+ const totalMessages = sessions.reduce((sum, s) => sum + s.messageCount, 0);
339
+ const now = Date.now();
340
+ const last24h = sessions.filter((s) => now - s.updatedAt < 24 * 60 * 60 * 1000).length;
341
+ const last7d = sessions.filter((s) => now - s.updatedAt < 7 * 24 * 60 * 60 * 1000).length;
342
+ const stats = {
343
+ totalSessions: sessions.length,
344
+ totalMessages,
345
+ sessionsLast24h: last24h,
346
+ sessionsLast7d: last7d,
347
+ oldestSession: sessions.length > 0 ? new Date(sessions[sessions.length - 1].createdAt).toISOString() : null,
348
+ newestSession: sessions.length > 0 ? new Date(sessions[0].updatedAt).toISOString() : null,
349
+ };
350
+ return {
351
+ content: [
352
+ {
353
+ type: "text",
354
+ text: JSON.stringify(stats, null, 2),
355
+ },
356
+ ],
357
+ };
358
+ }
359
+ case "send_message": {
360
+ if (this.options.readOnly) {
361
+ throw new Error("Server is in read-only mode");
362
+ }
363
+ const sessionKey = args?.sessionKey;
364
+ const message = args?.message;
365
+ if (!sessionKey || !message) {
366
+ throw new Error("sessionKey and message are required");
367
+ }
368
+ // Note: This would need integration with the actual gateway to send messages
369
+ // For now, return a placeholder response
370
+ return {
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text: `Message sent to session ${sessionKey}. Note: Full integration requires gateway connection.`,
375
+ },
376
+ ],
377
+ };
378
+ }
379
+ default:
380
+ throw new Error(`Unknown tool: ${name}`);
381
+ }
382
+ }
383
+ catch (error) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: "text",
388
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
389
+ },
390
+ ],
391
+ isError: true,
392
+ };
393
+ }
394
+ });
395
+ }
396
+ /**
397
+ * Start MCP server with stdio transport (for Claude Desktop, etc.)
398
+ */
399
+ async startStdio() {
400
+ const transport = new StdioServerTransport();
401
+ await this.server.connect(transport);
402
+ console.error("PoolBot MCP Server running on stdio");
403
+ }
404
+ /**
405
+ * Start MCP server with HTTP transport (for web clients)
406
+ */
407
+ async startHTTP(port) {
408
+ const httpPort = port || this.options.httpPort || 3000;
409
+ const transport = new StreamableHTTPServerTransport({
410
+ sessionIdGenerator: () => crypto.randomUUID(),
411
+ });
412
+ await this.server.connect(transport);
413
+ const server = createServer(async (req, res) => {
414
+ if (req.url === "/mcp") {
415
+ await transport.handleRequest(req, res);
416
+ }
417
+ else {
418
+ res.writeHead(404);
419
+ res.end("Not found");
420
+ }
421
+ });
422
+ await new Promise((resolve) => {
423
+ server.listen(httpPort, () => {
424
+ console.error(`PoolBot MCP Server running on http://localhost:${httpPort}/mcp`);
425
+ resolve();
426
+ });
427
+ });
428
+ }
429
+ /**
430
+ * Stop the MCP server
431
+ */
432
+ async stop() {
433
+ await this.server.close();
434
+ }
435
+ }
436
+ // ── CLI Entry Point ──────────────────────────────────────────────────
437
+ export async function runMCPServer(options = {}) {
438
+ const server = new PoolBotMCPServer(options);
439
+ const args = process.argv.slice(2);
440
+ const httpPort = args.find((arg) => arg.startsWith("--port="))?.split("=")[1];
441
+ const isHTTP = args.includes("--http") || !!httpPort;
442
+ try {
443
+ if (isHTTP) {
444
+ await server.startHTTP(httpPort ? parseInt(httpPort, 10) : undefined);
445
+ }
446
+ else {
447
+ await server.startStdio();
448
+ }
449
+ // Handle shutdown gracefully
450
+ process.on("SIGINT", async () => {
451
+ await server.stop();
452
+ process.exit(0);
453
+ });
454
+ process.on("SIGTERM", async () => {
455
+ await server.stop();
456
+ process.exit(0);
457
+ });
458
+ }
459
+ catch (error) {
460
+ console.error("Failed to start MCP server:", error);
461
+ process.exit(1);
462
+ }
463
+ }
@@ -0,0 +1,171 @@
1
+ # PoolBot MCP Server
2
+
3
+ ## Overview
4
+
5
+ The PoolBot MCP (Model Context Protocol) Server exposes your PoolBot sessions and conversations to MCP-compatible clients like:
6
+
7
+ - **Claude Desktop** - Chat directly with your PoolBot session history
8
+ - **Cursor** - Search and reference PoolBot conversations in your IDE
9
+ - **VS Code** - Access PoolBot context from your editor
10
+ - **Any MCP Client** - Standard protocol support
11
+
12
+ ## Features
13
+
14
+ ### Tools
15
+
16
+ | Tool | Description |
17
+ |------|-------------|
18
+ | `list_sessions` | List all PoolBot sessions with metadata |
19
+ | `get_session` | Get details of a specific session |
20
+ | `get_messages` | Get messages from a session (with optional role filter) |
21
+ | `search_sessions` | Search across all sessions for text |
22
+ | `get_session_stats` | Get statistics about your sessions |
23
+ | `send_message` | Send a message to a session (optional, read-only mode disables this) |
24
+
25
+ ### Resources
26
+
27
+ Sessions are exposed as MCP resources at URIs like:
28
+ ```
29
+ poolbot://session/<sessionKey>
30
+ ```
31
+
32
+ ## Installation
33
+
34
+ ### Claude Desktop
35
+
36
+ Add to your `claude_desktop_config.json`:
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "poolbot": {
42
+ "command": "poolbot",
43
+ "args": ["mcp", "serve"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Cursor / VS Code
50
+
51
+ Add to your MCP settings:
52
+
53
+ ```json
54
+ {
55
+ "mcp": {
56
+ "servers": {
57
+ "poolbot": {
58
+ "command": "poolbot",
59
+ "args": ["mcp", "serve"]
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ### Start MCP Server (stdio mode)
69
+
70
+ ```bash
71
+ poolbot mcp serve
72
+ ```
73
+
74
+ This is the default mode for Claude Desktop and most MCP clients.
75
+
76
+ ### Start MCP Server (HTTP mode)
77
+
78
+ ```bash
79
+ poolbot mcp serve --http
80
+ poolbot mcp serve --http --port 3001
81
+ ```
82
+
83
+ HTTP mode is useful for web-based MCP clients or remote access.
84
+
85
+ ### Read-Only Mode
86
+
87
+ ```bash
88
+ poolbot mcp serve --read-only
89
+ ```
90
+
91
+ Disables the `send_message` tool for security.
92
+
93
+ ## Examples
94
+
95
+ ### List Sessions
96
+
97
+ ```json
98
+ {
99
+ "name": "list_sessions",
100
+ "arguments": {
101
+ "limit": 20
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Search Sessions
107
+
108
+ ```json
109
+ {
110
+ "name": "search_sessions",
111
+ "arguments": {
112
+ "query": "deployment configuration",
113
+ "limit": 10
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### Get Messages
119
+
120
+ ```json
121
+ {
122
+ "name": "get_messages",
123
+ "arguments": {
124
+ "sessionKey": "abc123...",
125
+ "limit": 50,
126
+ "role": "assistant"
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Security
132
+
133
+ - MCP server runs locally by default
134
+ - No external network access required for stdio mode
135
+ - HTTP mode binds to localhost by default
136
+ - Use `--read-only` to disable message sending
137
+ - Sessions are read from your local PoolBot workspace
138
+
139
+ ## Troubleshooting
140
+
141
+ ### "Command not found: poolbot"
142
+
143
+ Ensure PoolBot is installed globally:
144
+ ```bash
145
+ npm install -g @poolzin/pool-bot
146
+ ```
147
+
148
+ ### "No sessions found"
149
+
150
+ Sessions are loaded from your PoolBot workspace. Ensure you have existing sessions in `~/.poolbot/memory/<agentId>/sessions/`.
151
+
152
+ ### HTTP mode not connecting
153
+
154
+ Ensure the port is not in use:
155
+ ```bash
156
+ poolbot mcp serve --http --port 3001
157
+ ```
158
+
159
+ ## Architecture
160
+
161
+ The MCP server:
162
+ 1. Reads sessions from PoolBot's session store (`sessions.json`)
163
+ 2. Loads messages from JSONL files (`sessions/<sessionKey>.jsonl`)
164
+ 3. Exposes tools and resources via the MCP protocol
165
+ 4. Supports both stdio (for local clients) and HTTP transports
166
+
167
+ ## See Also
168
+
169
+ - [Model Context Protocol Documentation](https://modelcontextprotocol.io/)
170
+ - [Claude Desktop MCP Setup](https://claude.ai/mcp)
171
+ - [PoolBot Documentation](/docs/)