stacks-ai 0.1.1
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 +93 -0
- package/bin/stacks.cjs +215 -0
- package/dist/assets/aiProvider-DYAx3DVK.js +1 -0
- package/dist/assets/index-C1agmKFP.js +76 -0
- package/dist/assets/index-C8w_QbzK.js +15 -0
- package/dist/assets/index-Dp9e0AZR.js +5252 -0
- package/dist/assets/index-Q3CD-OmM.css +1 -0
- package/dist/assets/indexedDB-dA1h4x9Q.js +1 -0
- package/dist/assets/visionAnalysis-BGbWI8eX.js +9 -0
- package/dist/fonts/frank-the-architect.ttf +0 -0
- package/dist/index.html +13 -0
- package/dist/mcp/proxy.js +404 -0
- package/dist/mcp/server.js +529 -0
- package/electron/main.cjs +183 -0
- package/package.json +86 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { existsSync, mkdirSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
// Database setup
|
|
10
|
+
const dataDir = join(homedir(), ".stacks");
|
|
11
|
+
if (!existsSync(dataDir)) {
|
|
12
|
+
mkdirSync(dataDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
const db = new Database(join(dataDir, "canvas.db"));
|
|
15
|
+
// Initialize schema
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS spaces (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
data JSON NOT NULL,
|
|
20
|
+
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
21
|
+
)
|
|
22
|
+
`);
|
|
23
|
+
// Database helpers
|
|
24
|
+
const loadSpace = (spaceId) => {
|
|
25
|
+
const row = db.prepare("SELECT data FROM spaces WHERE id = ?").get(spaceId);
|
|
26
|
+
return row ? JSON.parse(row.data) : null;
|
|
27
|
+
};
|
|
28
|
+
const saveSpace = (space) => {
|
|
29
|
+
db.prepare("INSERT OR REPLACE INTO spaces (id, data, updatedAt) VALUES (?, ?, CURRENT_TIMESTAMP)")
|
|
30
|
+
.run(space.id, JSON.stringify(space));
|
|
31
|
+
};
|
|
32
|
+
const listSpaces = () => {
|
|
33
|
+
const rows = db.prepare("SELECT data FROM spaces").all();
|
|
34
|
+
return rows.map(row => JSON.parse(row.data));
|
|
35
|
+
};
|
|
36
|
+
const deleteSpace = (spaceId) => {
|
|
37
|
+
const result = db.prepare("DELETE FROM spaces WHERE id = ?").run(spaceId);
|
|
38
|
+
return result.changes > 0;
|
|
39
|
+
};
|
|
40
|
+
// Ensure root space exists
|
|
41
|
+
if (!loadSpace("root")) {
|
|
42
|
+
saveSpace({
|
|
43
|
+
id: "root",
|
|
44
|
+
name: "Scratchpad",
|
|
45
|
+
parentId: null,
|
|
46
|
+
items: [],
|
|
47
|
+
connections: [],
|
|
48
|
+
camera: { x: 0, y: 0, zoom: 1 }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Item defaults
|
|
52
|
+
const ITEM_DEFAULTS = {
|
|
53
|
+
sticky: { w: 200, h: 200, color: "bg-yellow-200" },
|
|
54
|
+
note: { w: 300, h: 400 },
|
|
55
|
+
image: { w: 300, h: 200 },
|
|
56
|
+
video: { w: 300, h: 200 },
|
|
57
|
+
folder: { w: 200, h: 240 }
|
|
58
|
+
};
|
|
59
|
+
// Generate unique ID
|
|
60
|
+
const generateId = (prefix = "item") => {
|
|
61
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
62
|
+
};
|
|
63
|
+
// Create MCP Server
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: "stacks-canvas",
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
});
|
|
68
|
+
// ============================================
|
|
69
|
+
// TOOLS: Canvas Item Operations
|
|
70
|
+
// ============================================
|
|
71
|
+
server.tool("add_item", "Add a new item (sticky note, note, image, video, or folder) to a space", {
|
|
72
|
+
spaceId: z.string().describe("ID of the space to add item to (use 'root' for main space)"),
|
|
73
|
+
itemType: z.enum(["sticky", "note", "image", "video", "folder"]).describe("Type of item to create"),
|
|
74
|
+
x: z.number().describe("X coordinate on canvas"),
|
|
75
|
+
y: z.number().describe("Y coordinate on canvas"),
|
|
76
|
+
content: z.string().describe("Item content: text for sticky/note, URL for image/video, name for folder"),
|
|
77
|
+
width: z.number().optional().describe("Width (optional, uses default based on type)"),
|
|
78
|
+
height: z.number().optional().describe("Height (optional, uses default based on type)"),
|
|
79
|
+
rotation: z.number().optional().describe("Rotation in degrees (optional, default 0)"),
|
|
80
|
+
color: z.string().optional().describe("Tailwind color class e.g. 'bg-yellow-200' (optional)"),
|
|
81
|
+
}, async ({ spaceId, itemType, x, y, content, width, height, rotation, color }) => {
|
|
82
|
+
const space = loadSpace(spaceId);
|
|
83
|
+
if (!space) {
|
|
84
|
+
return { content: [{ type: "text", text: `Error: Space "${spaceId}" not found` }] };
|
|
85
|
+
}
|
|
86
|
+
const defaults = ITEM_DEFAULTS[itemType];
|
|
87
|
+
const maxZ = space.items.length > 0 ? Math.max(...space.items.map(i => i.zIndex)) : 0;
|
|
88
|
+
const newItem = {
|
|
89
|
+
id: generateId(itemType),
|
|
90
|
+
type: itemType,
|
|
91
|
+
x: x ?? 0,
|
|
92
|
+
y: y ?? 0,
|
|
93
|
+
w: width ?? defaults.w,
|
|
94
|
+
h: height ?? defaults.h,
|
|
95
|
+
zIndex: maxZ + 1,
|
|
96
|
+
rotation: rotation ?? (Math.random() - 0.5) * 6,
|
|
97
|
+
content,
|
|
98
|
+
color: color ?? defaults.color,
|
|
99
|
+
};
|
|
100
|
+
// If folder, create linked space
|
|
101
|
+
if (itemType === "folder") {
|
|
102
|
+
const linkedSpaceId = generateId("space");
|
|
103
|
+
newItem.linkedSpaceId = linkedSpaceId;
|
|
104
|
+
saveSpace({
|
|
105
|
+
id: linkedSpaceId,
|
|
106
|
+
name: content,
|
|
107
|
+
parentId: spaceId,
|
|
108
|
+
items: [],
|
|
109
|
+
connections: [],
|
|
110
|
+
camera: { x: 0, y: 0, zoom: 1 }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
space.items.push(newItem);
|
|
114
|
+
saveSpace(space);
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Created ${itemType} "${content.substring(0, 50)}${content.length > 50 ? '...' : ''}" at (${x}, ${y}) with ID: ${newItem.id}`
|
|
119
|
+
}]
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
server.tool("update_item", "Update an existing item's position, size, content, or properties", {
|
|
123
|
+
spaceId: z.string().describe("Space ID containing the item"),
|
|
124
|
+
itemId: z.string().describe("ID of item to update"),
|
|
125
|
+
x: z.number().optional().describe("New X position"),
|
|
126
|
+
y: z.number().optional().describe("New Y position"),
|
|
127
|
+
w: z.number().optional().describe("New width"),
|
|
128
|
+
h: z.number().optional().describe("New height"),
|
|
129
|
+
rotation: z.number().optional().describe("New rotation"),
|
|
130
|
+
content: z.string().optional().describe("New content"),
|
|
131
|
+
color: z.string().optional().describe("New color"),
|
|
132
|
+
zIndex: z.number().optional().describe("New z-index/layer"),
|
|
133
|
+
}, async ({ spaceId, itemId, ...updates }) => {
|
|
134
|
+
const space = loadSpace(spaceId);
|
|
135
|
+
if (!space) {
|
|
136
|
+
return { content: [{ type: "text", text: `Error: Space "${spaceId}" not found` }] };
|
|
137
|
+
}
|
|
138
|
+
const item = space.items.find(i => i.id === itemId);
|
|
139
|
+
if (!item) {
|
|
140
|
+
return { content: [{ type: "text", text: `Error: Item "${itemId}" not found` }] };
|
|
141
|
+
}
|
|
142
|
+
// Apply updates
|
|
143
|
+
const validUpdates = Object.entries(updates).filter(([_, v]) => v !== undefined);
|
|
144
|
+
for (const [key, value] of validUpdates) {
|
|
145
|
+
item[key] = value;
|
|
146
|
+
}
|
|
147
|
+
saveSpace(space);
|
|
148
|
+
return {
|
|
149
|
+
content: [{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: `Updated item ${itemId}: ${validUpdates.map(([k, v]) => `${k}=${v}`).join(", ")}`
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
server.tool("delete_item", "Delete an item from a space", {
|
|
156
|
+
spaceId: z.string().describe("Space ID"),
|
|
157
|
+
itemId: z.string().describe("Item ID to delete"),
|
|
158
|
+
}, async ({ spaceId, itemId }) => {
|
|
159
|
+
const space = loadSpace(spaceId);
|
|
160
|
+
if (!space) {
|
|
161
|
+
return { content: [{ type: "text", text: `Error: Space "${spaceId}" not found` }] };
|
|
162
|
+
}
|
|
163
|
+
const index = space.items.findIndex(i => i.id === itemId);
|
|
164
|
+
if (index === -1) {
|
|
165
|
+
return { content: [{ type: "text", text: `Error: Item "${itemId}" not found` }] };
|
|
166
|
+
}
|
|
167
|
+
const deleted = space.items.splice(index, 1)[0];
|
|
168
|
+
// Also remove any connections involving this item
|
|
169
|
+
space.connections = space.connections.filter(c => c.from !== itemId && c.to !== itemId);
|
|
170
|
+
// If folder, delete linked space
|
|
171
|
+
if (deleted.linkedSpaceId) {
|
|
172
|
+
deleteSpace(deleted.linkedSpaceId);
|
|
173
|
+
}
|
|
174
|
+
saveSpace(space);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: `Deleted ${deleted.type} "${deleted.content.substring(0, 30)}..."` }]
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
server.tool("move_item", "Move an item to a new position (convenience method)", {
|
|
180
|
+
spaceId: z.string().describe("Space ID"),
|
|
181
|
+
itemId: z.string().describe("Item ID"),
|
|
182
|
+
x: z.number().describe("New X position"),
|
|
183
|
+
y: z.number().describe("New Y position"),
|
|
184
|
+
}, async ({ spaceId, itemId, x, y }) => {
|
|
185
|
+
const space = loadSpace(spaceId);
|
|
186
|
+
if (!space) {
|
|
187
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
188
|
+
}
|
|
189
|
+
const item = space.items.find(i => i.id === itemId);
|
|
190
|
+
if (!item) {
|
|
191
|
+
return { content: [{ type: "text", text: `Error: Item not found` }] };
|
|
192
|
+
}
|
|
193
|
+
const oldPos = { x: item.x, y: item.y };
|
|
194
|
+
item.x = x;
|
|
195
|
+
item.y = y;
|
|
196
|
+
saveSpace(space);
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: `Moved item from (${oldPos.x}, ${oldPos.y}) to (${x}, ${y})` }]
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
// ============================================
|
|
202
|
+
// TOOLS: Connection Operations
|
|
203
|
+
// ============================================
|
|
204
|
+
server.tool("create_connection", "Create a visual connection/link between two items", {
|
|
205
|
+
spaceId: z.string().describe("Space ID"),
|
|
206
|
+
fromItemId: z.string().describe("Source item ID"),
|
|
207
|
+
toItemId: z.string().describe("Target item ID"),
|
|
208
|
+
}, async ({ spaceId, fromItemId, toItemId }) => {
|
|
209
|
+
const space = loadSpace(spaceId);
|
|
210
|
+
if (!space) {
|
|
211
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
212
|
+
}
|
|
213
|
+
// Verify both items exist
|
|
214
|
+
const fromItem = space.items.find(i => i.id === fromItemId);
|
|
215
|
+
const toItem = space.items.find(i => i.id === toItemId);
|
|
216
|
+
if (!fromItem || !toItem) {
|
|
217
|
+
return { content: [{ type: "text", text: `Error: One or both items not found` }] };
|
|
218
|
+
}
|
|
219
|
+
// Check for duplicate
|
|
220
|
+
const exists = space.connections.some(c => (c.from === fromItemId && c.to === toItemId) || (c.from === toItemId && c.to === fromItemId));
|
|
221
|
+
if (exists) {
|
|
222
|
+
return { content: [{ type: "text", text: `Connection already exists between these items` }] };
|
|
223
|
+
}
|
|
224
|
+
const connection = {
|
|
225
|
+
id: generateId("conn"),
|
|
226
|
+
from: fromItemId,
|
|
227
|
+
to: toItemId,
|
|
228
|
+
};
|
|
229
|
+
space.connections.push(connection);
|
|
230
|
+
saveSpace(space);
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `Created connection: ${fromItem.content.substring(0, 20)} -> ${toItem.content.substring(0, 20)}` }]
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
server.tool("delete_connection", "Delete a connection between items", {
|
|
236
|
+
spaceId: z.string().describe("Space ID"),
|
|
237
|
+
connectionId: z.string().describe("Connection ID to delete"),
|
|
238
|
+
}, async ({ spaceId, connectionId }) => {
|
|
239
|
+
const space = loadSpace(spaceId);
|
|
240
|
+
if (!space) {
|
|
241
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
242
|
+
}
|
|
243
|
+
const index = space.connections.findIndex(c => c.id === connectionId);
|
|
244
|
+
if (index === -1) {
|
|
245
|
+
return { content: [{ type: "text", text: `Error: Connection not found` }] };
|
|
246
|
+
}
|
|
247
|
+
space.connections.splice(index, 1);
|
|
248
|
+
saveSpace(space);
|
|
249
|
+
return { content: [{ type: "text", text: `Deleted connection ${connectionId}` }] };
|
|
250
|
+
});
|
|
251
|
+
// ============================================
|
|
252
|
+
// TOOLS: Space Management
|
|
253
|
+
// ============================================
|
|
254
|
+
server.tool("list_spaces", "List all available spaces", {}, async () => {
|
|
255
|
+
const spaces = listSpaces();
|
|
256
|
+
const summary = spaces.map(s => `- ${s.name} (${s.id}): ${s.items.length} items, ${s.connections.length} connections`).join("\n");
|
|
257
|
+
return {
|
|
258
|
+
content: [{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: `Found ${spaces.length} spaces:\n${summary}`
|
|
261
|
+
}]
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
server.tool("get_space", "Get detailed information about a specific space", {
|
|
265
|
+
spaceId: z.string().describe("Space ID to retrieve"),
|
|
266
|
+
}, async ({ spaceId }) => {
|
|
267
|
+
const space = loadSpace(spaceId);
|
|
268
|
+
if (!space) {
|
|
269
|
+
return { content: [{ type: "text", text: `Error: Space "${spaceId}" not found` }] };
|
|
270
|
+
}
|
|
271
|
+
const itemSummary = space.items.map(i => ` - [${i.type}] "${i.content.substring(0, 40)}${i.content.length > 40 ? '...' : ''}" at (${Math.round(i.x)}, ${Math.round(i.y)}) ID: ${i.id}`).join("\n");
|
|
272
|
+
const connSummary = space.connections.map(c => ` - ${c.from} -> ${c.to}`).join("\n");
|
|
273
|
+
return {
|
|
274
|
+
content: [{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: `Space: ${space.name} (${space.id})
|
|
277
|
+
Parent: ${space.parentId || "none (root level)"}
|
|
278
|
+
Camera: x=${space.camera.x}, y=${space.camera.y}, zoom=${space.camera.zoom}
|
|
279
|
+
|
|
280
|
+
Items (${space.items.length}):
|
|
281
|
+
${itemSummary || " (none)"}
|
|
282
|
+
|
|
283
|
+
Connections (${space.connections.length}):
|
|
284
|
+
${connSummary || " (none)"}`
|
|
285
|
+
}]
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
server.tool("create_space", "Create a new top-level space", {
|
|
289
|
+
name: z.string().describe("Name for the new space"),
|
|
290
|
+
}, async ({ name }) => {
|
|
291
|
+
const newSpace = {
|
|
292
|
+
id: generateId("space"),
|
|
293
|
+
name,
|
|
294
|
+
parentId: null,
|
|
295
|
+
items: [],
|
|
296
|
+
connections: [],
|
|
297
|
+
camera: { x: 0, y: 0, zoom: 1 }
|
|
298
|
+
};
|
|
299
|
+
saveSpace(newSpace);
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: `Created new space "${name}" with ID: ${newSpace.id}` }]
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
server.tool("delete_space", "Delete a space and all its contents (cannot delete root)", {
|
|
305
|
+
spaceId: z.string().describe("Space ID to delete"),
|
|
306
|
+
}, async ({ spaceId }) => {
|
|
307
|
+
if (spaceId === "root") {
|
|
308
|
+
return { content: [{ type: "text", text: `Error: Cannot delete root space` }] };
|
|
309
|
+
}
|
|
310
|
+
const space = loadSpace(spaceId);
|
|
311
|
+
if (!space) {
|
|
312
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
313
|
+
}
|
|
314
|
+
// Delete any linked sub-spaces from folders
|
|
315
|
+
for (const item of space.items) {
|
|
316
|
+
if (item.linkedSpaceId) {
|
|
317
|
+
deleteSpace(item.linkedSpaceId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
deleteSpace(spaceId);
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: "text", text: `Deleted space "${space.name}" and ${space.items.length} items` }]
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
server.tool("list_items", "List all items in a space with their positions and types", {
|
|
326
|
+
spaceId: z.string().describe("Space ID"),
|
|
327
|
+
}, async ({ spaceId }) => {
|
|
328
|
+
const space = loadSpace(spaceId);
|
|
329
|
+
if (!space) {
|
|
330
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
331
|
+
}
|
|
332
|
+
if (space.items.length === 0) {
|
|
333
|
+
return { content: [{ type: "text", text: `Space "${space.name}" is empty` }] };
|
|
334
|
+
}
|
|
335
|
+
const summary = space.items.map(i => {
|
|
336
|
+
const preview = i.content.substring(0, 50) + (i.content.length > 50 ? "..." : "");
|
|
337
|
+
return `- ${i.type} "${preview}" at (${Math.round(i.x)}, ${Math.round(i.y)}) [${i.id}]`;
|
|
338
|
+
}).join("\n");
|
|
339
|
+
return {
|
|
340
|
+
content: [{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: `Items in "${space.name}" (${space.items.length}):\n${summary}`
|
|
343
|
+
}]
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
// ============================================
|
|
347
|
+
// TOOLS: Bulk Operations
|
|
348
|
+
// ============================================
|
|
349
|
+
server.tool("organize_items", "Auto-organize items in a grid layout", {
|
|
350
|
+
spaceId: z.string().describe("Space ID"),
|
|
351
|
+
columns: z.number().optional().default(3).describe("Number of columns (default 3)"),
|
|
352
|
+
spacing: z.number().optional().default(40).describe("Spacing between items (default 40)"),
|
|
353
|
+
}, async ({ spaceId, columns = 3, spacing = 40 }) => {
|
|
354
|
+
const space = loadSpace(spaceId);
|
|
355
|
+
if (!space) {
|
|
356
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
357
|
+
}
|
|
358
|
+
if (space.items.length === 0) {
|
|
359
|
+
return { content: [{ type: "text", text: `No items to organize` }] };
|
|
360
|
+
}
|
|
361
|
+
// Sort by current position for stability
|
|
362
|
+
const sorted = [...space.items].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
363
|
+
// Calculate grid positions
|
|
364
|
+
let currentX = 0;
|
|
365
|
+
let currentY = 0;
|
|
366
|
+
let rowHeight = 0;
|
|
367
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
368
|
+
const item = sorted[i];
|
|
369
|
+
const original = space.items.find(it => it.id === item.id);
|
|
370
|
+
if (i > 0 && i % columns === 0) {
|
|
371
|
+
currentX = 0;
|
|
372
|
+
currentY += rowHeight + spacing;
|
|
373
|
+
rowHeight = 0;
|
|
374
|
+
}
|
|
375
|
+
original.x = currentX;
|
|
376
|
+
original.y = currentY;
|
|
377
|
+
original.rotation = 0; // Reset rotation for clean grid
|
|
378
|
+
rowHeight = Math.max(rowHeight, original.h);
|
|
379
|
+
currentX += original.w + spacing;
|
|
380
|
+
}
|
|
381
|
+
saveSpace(space);
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: "text", text: `Organized ${space.items.length} items into ${columns}-column grid` }]
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
server.tool("clear_space", "Remove all items from a space (keeps the space itself)", {
|
|
387
|
+
spaceId: z.string().describe("Space ID to clear"),
|
|
388
|
+
}, async ({ spaceId }) => {
|
|
389
|
+
const space = loadSpace(spaceId);
|
|
390
|
+
if (!space) {
|
|
391
|
+
return { content: [{ type: "text", text: `Error: Space not found` }] };
|
|
392
|
+
}
|
|
393
|
+
const count = space.items.length;
|
|
394
|
+
// Delete linked spaces from folders
|
|
395
|
+
for (const item of space.items) {
|
|
396
|
+
if (item.linkedSpaceId) {
|
|
397
|
+
deleteSpace(item.linkedSpaceId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
space.items = [];
|
|
401
|
+
space.connections = [];
|
|
402
|
+
saveSpace(space);
|
|
403
|
+
return {
|
|
404
|
+
content: [{ type: "text", text: `Cleared ${count} items from "${space.name}"` }]
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
// ============================================
|
|
408
|
+
// RESOURCES: Canvas State
|
|
409
|
+
// ============================================
|
|
410
|
+
server.resource("canvas://spaces", "List of all spaces", async (uri) => {
|
|
411
|
+
const spaces = listSpaces();
|
|
412
|
+
return {
|
|
413
|
+
contents: [{
|
|
414
|
+
uri: uri.href,
|
|
415
|
+
mimeType: "application/json",
|
|
416
|
+
text: JSON.stringify(spaces.map(s => ({
|
|
417
|
+
id: s.id,
|
|
418
|
+
name: s.name,
|
|
419
|
+
parentId: s.parentId,
|
|
420
|
+
itemCount: s.items.length,
|
|
421
|
+
connectionCount: s.connections.length
|
|
422
|
+
})), null, 2)
|
|
423
|
+
}]
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
// ============================================
|
|
427
|
+
// PROMPTS: AI Workflows
|
|
428
|
+
// ============================================
|
|
429
|
+
server.prompt("brainstorm", "Generate ideas and add them as sticky notes to the canvas", {
|
|
430
|
+
topic: z.string().describe("Topic to brainstorm about"),
|
|
431
|
+
count: z.string().optional().default("5").describe("Number of ideas to generate"),
|
|
432
|
+
}, async ({ topic, count = "5" }) => {
|
|
433
|
+
return {
|
|
434
|
+
messages: [{
|
|
435
|
+
role: "user",
|
|
436
|
+
content: {
|
|
437
|
+
type: "text",
|
|
438
|
+
text: `Generate ${count} creative ideas about "${topic}".
|
|
439
|
+
|
|
440
|
+
For each idea, use the add_item tool to create a sticky note in the "root" space.
|
|
441
|
+
Space them out in a roughly circular pattern around center (0,0), about 300 pixels apart.
|
|
442
|
+
Use different pastel colors: bg-yellow-200, bg-pink-200, bg-green-200, bg-blue-200, bg-purple-200.
|
|
443
|
+
|
|
444
|
+
Make the ideas concise but insightful. Each sticky note content should be 1-2 sentences.`
|
|
445
|
+
}
|
|
446
|
+
}]
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
server.prompt("summarize_space", "Analyze and summarize the contents of a space", {
|
|
450
|
+
spaceId: z.string().describe("Space ID to analyze"),
|
|
451
|
+
}, async ({ spaceId }) => {
|
|
452
|
+
const space = loadSpace(spaceId);
|
|
453
|
+
if (!space) {
|
|
454
|
+
return {
|
|
455
|
+
messages: [{
|
|
456
|
+
role: "user",
|
|
457
|
+
content: { type: "text", text: `Space "${spaceId}" not found.` }
|
|
458
|
+
}]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const itemsJson = JSON.stringify(space.items, null, 2);
|
|
462
|
+
return {
|
|
463
|
+
messages: [{
|
|
464
|
+
role: "user",
|
|
465
|
+
content: {
|
|
466
|
+
type: "text",
|
|
467
|
+
text: `Analyze this canvas space and provide a summary:
|
|
468
|
+
|
|
469
|
+
Space: ${space.name}
|
|
470
|
+
Items: ${space.items.length}
|
|
471
|
+
Connections: ${space.connections.length}
|
|
472
|
+
|
|
473
|
+
Item details:
|
|
474
|
+
${itemsJson}
|
|
475
|
+
|
|
476
|
+
Please provide:
|
|
477
|
+
1. A brief overview of what this space contains
|
|
478
|
+
2. Key themes or topics identified
|
|
479
|
+
3. Suggestions for organization or additions`
|
|
480
|
+
}
|
|
481
|
+
}]
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
server.prompt("layout_suggestions", "Get suggestions for organizing canvas items", {
|
|
485
|
+
spaceId: z.string().describe("Space to analyze"),
|
|
486
|
+
}, async ({ spaceId }) => {
|
|
487
|
+
const space = loadSpace(spaceId);
|
|
488
|
+
if (!space) {
|
|
489
|
+
return {
|
|
490
|
+
messages: [{
|
|
491
|
+
role: "user",
|
|
492
|
+
content: { type: "text", text: `Space "${spaceId}" not found.` }
|
|
493
|
+
}]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
messages: [{
|
|
498
|
+
role: "user",
|
|
499
|
+
content: {
|
|
500
|
+
type: "text",
|
|
501
|
+
text: `I have ${space.items.length} items on my canvas. Here are their current positions:
|
|
502
|
+
|
|
503
|
+
${space.items.map(i => `- ${i.type}: "${i.content.substring(0, 30)}" at (${i.x}, ${i.y})`).join("\n")}
|
|
504
|
+
|
|
505
|
+
Suggest a better visual arrangement. Consider:
|
|
506
|
+
- Grouping related items
|
|
507
|
+
- Creating visual hierarchy
|
|
508
|
+
- Using space effectively
|
|
509
|
+
- Making connections clear
|
|
510
|
+
|
|
511
|
+
You can use the move_item or organize_items tools to implement your suggestions.`
|
|
512
|
+
}
|
|
513
|
+
}]
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
// ============================================
|
|
517
|
+
// Server Startup
|
|
518
|
+
// ============================================
|
|
519
|
+
async function main() {
|
|
520
|
+
const transport = new StdioServerTransport();
|
|
521
|
+
await server.connect(transport);
|
|
522
|
+
console.error("Stacks Canvas MCP Server v1.0.0 running on stdio");
|
|
523
|
+
console.error(`Database: ${join(dataDir, "canvas.db")}`);
|
|
524
|
+
}
|
|
525
|
+
main().catch(error => {
|
|
526
|
+
console.error("Server startup error:", error);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
});
|
|
529
|
+
//# sourceMappingURL=server.js.map
|