lystbot 0.2.3 โ 0.3.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/README.md +72 -2
- package/package.json +2 -1
- package/src/index.js +9 -0
- package/src/mcp.js +229 -0
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LystBot CLI & MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Smart lists that your AI can actually use.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx lystbot login <your-api-key>
|
|
9
|
+
npx lystbot lists
|
|
10
|
+
npx lystbot add "Groceries" "Milk, Eggs, Butter"
|
|
11
|
+
npx lystbot check "Groceries" "Milk"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## MCP Server (Claude Desktop, Cursor, Windsurf)
|
|
15
|
+
|
|
16
|
+
LystBot includes a built-in MCP server. Add it to your AI tool:
|
|
17
|
+
|
|
18
|
+
### Claude Desktop
|
|
19
|
+
|
|
20
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"lystbot": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["lystbot", "mcp"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Cursor / Windsurf
|
|
34
|
+
|
|
35
|
+
Add to `.cursor/mcp.json` or `.windsurf/mcp.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"lystbot": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["lystbot", "mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Setup
|
|
49
|
+
|
|
50
|
+
1. Install the app: [lystbot.com](https://lystbot.com)
|
|
51
|
+
2. Copy your API key from Settings
|
|
52
|
+
3. Run `npx lystbot login <your-api-key>`
|
|
53
|
+
4. Add the MCP config above
|
|
54
|
+
5. Ask Claude: "What's on my grocery list?"
|
|
55
|
+
|
|
56
|
+
### Available Tools
|
|
57
|
+
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `list_lists` | Get all your lists |
|
|
61
|
+
| `get_list` | Get a list with all items |
|
|
62
|
+
| `create_list` | Create a new list |
|
|
63
|
+
| `delete_list` | Delete a list |
|
|
64
|
+
| `add_items` | Add items (comma-separated) |
|
|
65
|
+
| `check_item` | Check off an item |
|
|
66
|
+
| `uncheck_item` | Reopen a checked item |
|
|
67
|
+
| `remove_item` | Delete an item |
|
|
68
|
+
| `share_list` | Generate a share code |
|
|
69
|
+
| `join_list` | Join a shared list |
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
Full docs at [lystbot.com](https://lystbot.com) and [docs/](../docs/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lystbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "LystBot CLI - Manage your lists from the terminal",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"author": "TourAround UG",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
25
|
"commander": "^12.0.0"
|
|
25
26
|
},
|
|
26
27
|
"engines": {
|
package/src/index.js
CHANGED
|
@@ -391,4 +391,13 @@ program
|
|
|
391
391
|
console.log(`๐ค Joined: ${list.emoji || '๐'} ${list.title || list.name} (${list.item_count || 0} items)`);
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
+
// โโ mcp โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
395
|
+
program
|
|
396
|
+
.command('mcp')
|
|
397
|
+
.description('Start MCP (Model Context Protocol) server for Claude Desktop, Cursor, etc.')
|
|
398
|
+
.action(async () => {
|
|
399
|
+
const { startMcpServer } = require('./mcp');
|
|
400
|
+
await startMcpServer();
|
|
401
|
+
});
|
|
402
|
+
|
|
394
403
|
program.parse();
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
2
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
// --- API helper (non-exiting version for MCP) ---
|
|
6
|
+
async function api(method, path, body = null) {
|
|
7
|
+
const baseUrl = config.getBaseUrl();
|
|
8
|
+
const url = `${baseUrl}${path}`;
|
|
9
|
+
const cfg = config.read();
|
|
10
|
+
|
|
11
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
12
|
+
if (cfg?.apiKey) headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
13
|
+
|
|
14
|
+
const opts = { method, headers };
|
|
15
|
+
if (body) opts.body = JSON.stringify(body);
|
|
16
|
+
|
|
17
|
+
const res = await fetch(url, opts);
|
|
18
|
+
if (res.status === 204) return null;
|
|
19
|
+
|
|
20
|
+
const data = await res.json().catch(() => null);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const msg = data?.error?.message || data?.message || `HTTP ${res.status}`;
|
|
23
|
+
throw new Error(msg);
|
|
24
|
+
}
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findList(lists, query) {
|
|
29
|
+
const lower = query.toLowerCase();
|
|
30
|
+
return lists.find(l => l.id === query)
|
|
31
|
+
|| lists.find(l => (l.title || '').toLowerCase() === lower)
|
|
32
|
+
|| lists.find(l => (l.title || '').toLowerCase().startsWith(lower))
|
|
33
|
+
|| lists.find(l => (l.title || '').toLowerCase().includes(lower))
|
|
34
|
+
|| null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uuid() {
|
|
38
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
39
|
+
const r = Math.random() * 16 | 0;
|
|
40
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- MCP Server ---
|
|
45
|
+
async function startMcpServer() {
|
|
46
|
+
const cfg = config.read();
|
|
47
|
+
if (!cfg?.apiKey) {
|
|
48
|
+
console.error('Not logged in. Run: lystbot login <api-key>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const server = new McpServer({
|
|
53
|
+
name: 'lystbot',
|
|
54
|
+
version: require('../package.json').version,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// --- Tools ---
|
|
58
|
+
|
|
59
|
+
server.tool('list_lists', 'Get all lists', {}, async () => {
|
|
60
|
+
const data = await api('GET', '/lists');
|
|
61
|
+
const lists = data.lists || data;
|
|
62
|
+
const text = lists.length === 0
|
|
63
|
+
? 'No lists found.'
|
|
64
|
+
: lists.map(l => {
|
|
65
|
+
const emoji = l.emoji || '๐';
|
|
66
|
+
const count = l.item_count ?? '?';
|
|
67
|
+
return `${emoji} ${l.title} (${count} items) [id: ${l.id}]`;
|
|
68
|
+
}).join('\n');
|
|
69
|
+
return { content: [{ type: 'text', text }] };
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
server.tool('get_list', 'Get a list with all its items', {
|
|
73
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
74
|
+
}, async ({ list: query }) => {
|
|
75
|
+
const all = await api('GET', '/lists');
|
|
76
|
+
const match = findList(all.lists || all, query);
|
|
77
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
78
|
+
|
|
79
|
+
const detail = await api('GET', `/lists/${match.id}`);
|
|
80
|
+
const items = detail.items || [];
|
|
81
|
+
|
|
82
|
+
let text = `${match.emoji || '๐'} ${match.title}\n`;
|
|
83
|
+
text += `Type: ${match.type || 'generic'} | ${items.length} items\n\n`;
|
|
84
|
+
|
|
85
|
+
if (items.length === 0) {
|
|
86
|
+
text += '(empty)';
|
|
87
|
+
} else {
|
|
88
|
+
text += items.map(i => {
|
|
89
|
+
const check = i.checked ? 'โ
' : 'โฌ';
|
|
90
|
+
const qty = (i.quantity || 1) > 1 ? ` (${i.quantity}x)` : '';
|
|
91
|
+
const unit = i.unit ? ` ${i.unit}` : '';
|
|
92
|
+
return `${check} ${i.text}${qty}${unit}`;
|
|
93
|
+
}).join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { content: [{ type: 'text', text }] };
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.tool('create_list', 'Create a new list', {
|
|
100
|
+
title: { type: 'string', description: 'List title' },
|
|
101
|
+
type: { type: 'string', description: 'List type: shopping, todo, packing, or generic (default: generic)' },
|
|
102
|
+
emoji: { type: 'string', description: 'List emoji (optional, auto-picked if omitted)' },
|
|
103
|
+
}, async ({ title, type, emoji }) => {
|
|
104
|
+
const id = uuid();
|
|
105
|
+
if (!emoji) {
|
|
106
|
+
const lower = title.toLowerCase();
|
|
107
|
+
const map = {
|
|
108
|
+
grocer: '๐', shopping: '๐๏ธ', food: '๐', todo: 'โ
', task: '๐',
|
|
109
|
+
travel: 'โ๏ธ', trip: '๐งณ', pack: '๐งณ', movie: '๐ฌ', book: '๐',
|
|
110
|
+
gift: '๐', wish: 'โญ', fitness: '๐ช', recipe: '๐จโ๐ณ', clean: '๐งน',
|
|
111
|
+
};
|
|
112
|
+
emoji = Object.entries(map).find(([k]) => lower.includes(k))?.[1] || '๐';
|
|
113
|
+
}
|
|
114
|
+
await api('POST', '/lists', { id, title, type: type || 'generic', emoji });
|
|
115
|
+
return { content: [{ type: 'text', text: `Created: ${emoji} ${title} [id: ${id}]` }] };
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
server.tool('delete_list', 'Delete a list', {
|
|
119
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
120
|
+
}, async ({ list: query }) => {
|
|
121
|
+
const all = await api('GET', '/lists');
|
|
122
|
+
const match = findList(all.lists || all, query);
|
|
123
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
124
|
+
await api('DELETE', `/lists/${match.id}`);
|
|
125
|
+
return { content: [{ type: 'text', text: `Deleted: ${match.emoji || '๐'} ${match.title}` }] };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
server.tool('add_items', 'Add one or more items to a list', {
|
|
129
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
130
|
+
items: { type: 'string', description: 'Items to add, separated by comma+space or semicolon. Example: "Milk, Eggs, Butter"' },
|
|
131
|
+
}, async ({ list: query, items: itemsStr }) => {
|
|
132
|
+
const all = await api('GET', '/lists');
|
|
133
|
+
const match = findList(all.lists || all, query);
|
|
134
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
135
|
+
|
|
136
|
+
// Split on ", " or ";"
|
|
137
|
+
const texts = itemsStr.split(/;\s*|,\s+/).map(s => s.trim()).filter(Boolean);
|
|
138
|
+
const added = [];
|
|
139
|
+
|
|
140
|
+
for (const text of texts) {
|
|
141
|
+
const id = uuid();
|
|
142
|
+
await api('POST', `/lists/${match.id}/items`, { id, text });
|
|
143
|
+
added.push(text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { content: [{ type: 'text', text: `Added ${added.length} item(s) to ${match.emoji || '๐'} ${match.title}:\n${added.map(t => ` + ${t}`).join('\n')}` }] };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.tool('check_item', 'Check off (complete) an item', {
|
|
150
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
151
|
+
item: { type: 'string', description: 'Item text (fuzzy match)' },
|
|
152
|
+
}, async ({ list: query, item: itemQuery }) => {
|
|
153
|
+
const all = await api('GET', '/lists');
|
|
154
|
+
const match = findList(all.lists || all, query);
|
|
155
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
156
|
+
|
|
157
|
+
const detail = await api('GET', `/lists/${match.id}`);
|
|
158
|
+
const items = detail.items || [];
|
|
159
|
+
const lower = itemQuery.toLowerCase();
|
|
160
|
+
const found = items.find(i => i.text.toLowerCase() === lower)
|
|
161
|
+
|| items.find(i => i.text.toLowerCase().includes(lower));
|
|
162
|
+
if (!found) throw new Error(`Item "${itemQuery}" not found in ${match.title}`);
|
|
163
|
+
|
|
164
|
+
await api('PUT', `/lists/${match.id}/items/${found.id}`, { checked: true });
|
|
165
|
+
return { content: [{ type: 'text', text: `โ
Checked: ${found.text}` }] };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
server.tool('uncheck_item', 'Uncheck (reopen) an item', {
|
|
169
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
170
|
+
item: { type: 'string', description: 'Item text (fuzzy match)' },
|
|
171
|
+
}, async ({ list: query, item: itemQuery }) => {
|
|
172
|
+
const all = await api('GET', '/lists');
|
|
173
|
+
const match = findList(all.lists || all, query);
|
|
174
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
175
|
+
|
|
176
|
+
const detail = await api('GET', `/lists/${match.id}`);
|
|
177
|
+
const items = detail.items || [];
|
|
178
|
+
const lower = itemQuery.toLowerCase();
|
|
179
|
+
const found = items.find(i => i.text.toLowerCase() === lower)
|
|
180
|
+
|| items.find(i => i.text.toLowerCase().includes(lower));
|
|
181
|
+
if (!found) throw new Error(`Item "${itemQuery}" not found in ${match.title}`);
|
|
182
|
+
|
|
183
|
+
await api('PUT', `/lists/${match.id}/items/${found.id}`, { checked: false });
|
|
184
|
+
return { content: [{ type: 'text', text: `โฌ Unchecked: ${found.text}` }] };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
server.tool('remove_item', 'Remove an item from a list', {
|
|
188
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
189
|
+
item: { type: 'string', description: 'Item text (fuzzy match)' },
|
|
190
|
+
}, async ({ list: query, item: itemQuery }) => {
|
|
191
|
+
const all = await api('GET', '/lists');
|
|
192
|
+
const match = findList(all.lists || all, query);
|
|
193
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
194
|
+
|
|
195
|
+
const detail = await api('GET', `/lists/${match.id}`);
|
|
196
|
+
const items = detail.items || [];
|
|
197
|
+
const lower = itemQuery.toLowerCase();
|
|
198
|
+
const found = items.find(i => i.text.toLowerCase() === lower)
|
|
199
|
+
|| items.find(i => i.text.toLowerCase().includes(lower));
|
|
200
|
+
if (!found) throw new Error(`Item "${itemQuery}" not found in ${match.title}`);
|
|
201
|
+
|
|
202
|
+
await api('DELETE', `/lists/${match.id}/items/${found.id}`);
|
|
203
|
+
return { content: [{ type: 'text', text: `๐๏ธ Removed: ${found.text}` }] };
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
server.tool('share_list', 'Generate a share code for a list', {
|
|
207
|
+
list: { type: 'string', description: 'List name or ID' },
|
|
208
|
+
}, async ({ list: query }) => {
|
|
209
|
+
const all = await api('GET', '/lists');
|
|
210
|
+
const match = findList(all.lists || all, query);
|
|
211
|
+
if (!match) throw new Error(`List "${query}" not found`);
|
|
212
|
+
|
|
213
|
+
const data = await api('POST', `/lists/${match.id}/share`);
|
|
214
|
+
return { content: [{ type: 'text', text: `Share code for ${match.emoji || '๐'} ${match.title}: ${data.share_code}\nAnyone with this code can join the list.` }] };
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
server.tool('join_list', 'Join a shared list using a share code', {
|
|
218
|
+
code: { type: 'string', description: 'The share code' },
|
|
219
|
+
}, async ({ code }) => {
|
|
220
|
+
const data = await api('POST', '/lists/join', { share_code: code });
|
|
221
|
+
return { content: [{ type: 'text', text: `Joined list: ${data.emoji || '๐'} ${data.title || data.list_id}` }] };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Connect via stdio
|
|
225
|
+
const transport = new StdioServerTransport();
|
|
226
|
+
await server.connect(transport);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { startMcpServer };
|