slyplan-mcp 1.0.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/db.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { TreeNode, Link } from './types.js';
2
+ export declare function getTree(rootId?: string | null): Promise<TreeNode[]>;
3
+ export declare function getNode(id: string): Promise<TreeNode | null>;
4
+ export declare function insertNode(data: {
5
+ parentId: string | null;
6
+ type: string;
7
+ title: string;
8
+ description?: string;
9
+ status?: string;
10
+ progress?: number;
11
+ metadata?: Record<string, unknown>;
12
+ }): Promise<TreeNode>;
13
+ export declare function updateNode(id: string, updates: {
14
+ title?: string;
15
+ description?: string;
16
+ status?: string;
17
+ progress?: number;
18
+ collapsed?: boolean;
19
+ metadata?: Record<string, unknown>;
20
+ }): Promise<TreeNode | null>;
21
+ export declare function deleteNode(id: string): Promise<boolean>;
22
+ export declare function moveNode(id: string, newParentId: string | null, sortOrder?: number): Promise<TreeNode | null>;
23
+ export declare function addLink(nodeId: string, url: string, label?: string, linkType?: string): Promise<Link>;
24
+ export declare function getWorkMode(): Promise<TreeNode[]>;
25
+ export declare function addToWorkMode(nodeId: string): Promise<boolean>;
26
+ export declare function removeFromWorkMode(nodeId: string): Promise<boolean>;
27
+ export declare function searchNodes(query: string, filters?: {
28
+ type?: string[];
29
+ status?: string[];
30
+ }): Promise<TreeNode[]>;
package/dist/db.js ADDED
@@ -0,0 +1,305 @@
1
+ import { supabase, getUserId } from './supabase.js';
2
+ import { v4 as uuid } from 'uuid';
3
+ function dbNodeToTree(row, links, children) {
4
+ return {
5
+ id: row.id,
6
+ parentId: row.parent_id,
7
+ type: row.type,
8
+ title: row.title,
9
+ description: row.description,
10
+ status: row.status,
11
+ progress: row.progress,
12
+ sortOrder: row.sort_order,
13
+ collapsed: row.collapsed,
14
+ metadata: row.metadata || {},
15
+ links,
16
+ children,
17
+ createdAt: row.created_at,
18
+ updatedAt: row.updated_at,
19
+ };
20
+ }
21
+ function dbLinkToLink(row) {
22
+ return {
23
+ id: row.id,
24
+ nodeId: row.node_id,
25
+ url: row.url,
26
+ label: row.label,
27
+ linkType: row.link_type,
28
+ createdAt: row.created_at,
29
+ };
30
+ }
31
+ // --- Node CRUD ---
32
+ export async function getTree(rootId) {
33
+ const { data: allNodes, error: nodesErr } = await supabase
34
+ .from('nodes')
35
+ .select('*')
36
+ .order('sort_order', { ascending: true });
37
+ if (nodesErr)
38
+ throw new Error(nodesErr.message);
39
+ const { data: allLinks, error: linksErr } = await supabase
40
+ .from('links')
41
+ .select('*');
42
+ if (linksErr)
43
+ throw new Error(linksErr.message);
44
+ const linksByNode = new Map();
45
+ for (const link of allLinks || []) {
46
+ const arr = linksByNode.get(link.node_id) || [];
47
+ arr.push(dbLinkToLink(link));
48
+ linksByNode.set(link.node_id, arr);
49
+ }
50
+ const nodeMap = new Map();
51
+ const childrenMap = new Map();
52
+ for (const node of (allNodes || [])) {
53
+ nodeMap.set(node.id, node);
54
+ const parentKey = node.parent_id;
55
+ const arr = childrenMap.get(parentKey) || [];
56
+ arr.push(node);
57
+ childrenMap.set(parentKey, arr);
58
+ }
59
+ function buildSubtree(parentId) {
60
+ const children = childrenMap.get(parentId) || [];
61
+ return children.map(node => {
62
+ const subtreeChildren = buildSubtree(node.id);
63
+ const links = linksByNode.get(node.id) || [];
64
+ let progress = node.progress;
65
+ if (subtreeChildren.length > 0) {
66
+ const total = subtreeChildren.reduce((sum, c) => sum + c.progress, 0);
67
+ progress = Math.round(total / subtreeChildren.length);
68
+ }
69
+ return dbNodeToTree({ ...node, progress }, links, subtreeChildren);
70
+ });
71
+ }
72
+ if (rootId) {
73
+ const rootNode = nodeMap.get(rootId);
74
+ if (!rootNode)
75
+ return [];
76
+ const children = buildSubtree(rootId);
77
+ let progress = rootNode.progress;
78
+ if (children.length > 0) {
79
+ progress = Math.round(children.reduce((sum, c) => sum + c.progress, 0) / children.length);
80
+ }
81
+ return [dbNodeToTree({ ...rootNode, progress }, linksByNode.get(rootId) || [], children)];
82
+ }
83
+ return buildSubtree(null);
84
+ }
85
+ export async function getNode(id) {
86
+ const { data: row, error } = await supabase
87
+ .from('nodes')
88
+ .select('*')
89
+ .eq('id', id)
90
+ .single();
91
+ if (error || !row)
92
+ return null;
93
+ const { data: linkRows } = await supabase
94
+ .from('links')
95
+ .select('*')
96
+ .eq('node_id', id);
97
+ const links = (linkRows || []).map(dbLinkToLink);
98
+ const { data: childRows } = await supabase
99
+ .from('nodes')
100
+ .select('*')
101
+ .eq('parent_id', id)
102
+ .order('sort_order', { ascending: true });
103
+ const children = [];
104
+ for (const child of (childRows || [])) {
105
+ const { data: childLinkRows } = await supabase
106
+ .from('links')
107
+ .select('*')
108
+ .eq('node_id', child.id);
109
+ children.push(dbNodeToTree(child, (childLinkRows || []).map(dbLinkToLink), []));
110
+ }
111
+ let progress = row.progress;
112
+ if (children.length > 0) {
113
+ progress = Math.round(children.reduce((sum, c) => sum + c.progress, 0) / children.length);
114
+ }
115
+ return dbNodeToTree({ ...row, progress }, links, children);
116
+ }
117
+ export async function insertNode(data) {
118
+ const id = uuid();
119
+ let maxOrder = -1;
120
+ if (data.parentId === null) {
121
+ const { data: rootRows } = await supabase
122
+ .from('nodes')
123
+ .select('sort_order')
124
+ .is('parent_id', null)
125
+ .order('sort_order', { ascending: false })
126
+ .limit(1);
127
+ maxOrder = rootRows && rootRows.length > 0 ? rootRows[0].sort_order : -1;
128
+ }
129
+ else {
130
+ const { data: rows } = await supabase
131
+ .from('nodes')
132
+ .select('sort_order')
133
+ .eq('parent_id', data.parentId)
134
+ .order('sort_order', { ascending: false })
135
+ .limit(1);
136
+ maxOrder = rows && rows.length > 0 ? rows[0].sort_order : -1;
137
+ }
138
+ const { error } = await supabase.from('nodes').insert({
139
+ id,
140
+ parent_id: data.parentId,
141
+ type: data.type,
142
+ title: data.title,
143
+ description: data.description || '',
144
+ status: data.status || 'not_started',
145
+ progress: data.progress || 0,
146
+ sort_order: maxOrder + 1,
147
+ metadata: data.metadata || {},
148
+ created_by: getUserId(),
149
+ });
150
+ if (error)
151
+ throw new Error(error.message);
152
+ return (await getNode(id));
153
+ }
154
+ export async function updateNode(id, updates) {
155
+ const { data: existing } = await supabase
156
+ .from('nodes')
157
+ .select('id')
158
+ .eq('id', id)
159
+ .single();
160
+ if (!existing)
161
+ return null;
162
+ const fields = {};
163
+ if (updates.title !== undefined)
164
+ fields.title = updates.title;
165
+ if (updates.description !== undefined)
166
+ fields.description = updates.description;
167
+ if (updates.status !== undefined)
168
+ fields.status = updates.status;
169
+ if (updates.progress !== undefined)
170
+ fields.progress = updates.progress;
171
+ if (updates.collapsed !== undefined)
172
+ fields.collapsed = updates.collapsed;
173
+ if (updates.metadata !== undefined)
174
+ fields.metadata = updates.metadata;
175
+ if (Object.keys(fields).length === 0)
176
+ return getNode(id);
177
+ const { error } = await supabase
178
+ .from('nodes')
179
+ .update(fields)
180
+ .eq('id', id);
181
+ if (error)
182
+ throw new Error(error.message);
183
+ return getNode(id);
184
+ }
185
+ export async function deleteNode(id) {
186
+ const { data: existing } = await supabase
187
+ .from('nodes')
188
+ .select('id')
189
+ .eq('id', id)
190
+ .single();
191
+ if (!existing)
192
+ return false;
193
+ const { error } = await supabase.from('nodes').delete().eq('id', id);
194
+ if (error)
195
+ throw new Error(error.message);
196
+ return true;
197
+ }
198
+ export async function moveNode(id, newParentId, sortOrder) {
199
+ const { data: existing } = await supabase
200
+ .from('nodes')
201
+ .select('id')
202
+ .eq('id', id)
203
+ .single();
204
+ if (!existing)
205
+ return null;
206
+ let order = sortOrder;
207
+ if (order === undefined) {
208
+ if (newParentId === null) {
209
+ const { data: rootRows } = await supabase
210
+ .from('nodes')
211
+ .select('sort_order')
212
+ .is('parent_id', null)
213
+ .order('sort_order', { ascending: false })
214
+ .limit(1);
215
+ order = rootRows && rootRows.length > 0 ? rootRows[0].sort_order + 1 : 0;
216
+ }
217
+ else {
218
+ const { data: rows } = await supabase
219
+ .from('nodes')
220
+ .select('sort_order')
221
+ .eq('parent_id', newParentId)
222
+ .order('sort_order', { ascending: false })
223
+ .limit(1);
224
+ order = rows && rows.length > 0 ? rows[0].sort_order + 1 : 0;
225
+ }
226
+ }
227
+ const { error } = await supabase
228
+ .from('nodes')
229
+ .update({ parent_id: newParentId, sort_order: order })
230
+ .eq('id', id);
231
+ if (error)
232
+ throw new Error(error.message);
233
+ return getNode(id);
234
+ }
235
+ // --- Links ---
236
+ export async function addLink(nodeId, url, label, linkType) {
237
+ const id = uuid();
238
+ const { error } = await supabase.from('links').insert({
239
+ id,
240
+ node_id: nodeId,
241
+ url,
242
+ label: label || '',
243
+ link_type: linkType || 'reference',
244
+ });
245
+ if (error)
246
+ throw new Error(error.message);
247
+ return { id, nodeId, url, label: label || '', linkType: linkType || 'reference', createdAt: new Date().toISOString() };
248
+ }
249
+ // --- Work Mode ---
250
+ export async function getWorkMode() {
251
+ const { data: wmRows } = await supabase
252
+ .from('work_mode')
253
+ .select('node_id')
254
+ .eq('user_id', getUserId())
255
+ .order('added_at', { ascending: true });
256
+ if (!wmRows || wmRows.length === 0)
257
+ return [];
258
+ const nodeIds = wmRows.map(r => r.node_id);
259
+ const { data: nodeRows } = await supabase
260
+ .from('nodes')
261
+ .select('*')
262
+ .in('id', nodeIds);
263
+ const results = [];
264
+ for (const row of (nodeRows || [])) {
265
+ const { data: linkRows } = await supabase
266
+ .from('links')
267
+ .select('*')
268
+ .eq('node_id', row.id);
269
+ results.push(dbNodeToTree(row, (linkRows || []).map(dbLinkToLink), []));
270
+ }
271
+ return results;
272
+ }
273
+ export async function addToWorkMode(nodeId) {
274
+ const { error } = await supabase
275
+ .from('work_mode')
276
+ .upsert({ node_id: nodeId, user_id: getUserId() }, { onConflict: 'node_id,user_id' });
277
+ return !error;
278
+ }
279
+ export async function removeFromWorkMode(nodeId) {
280
+ const { error } = await supabase
281
+ .from('work_mode')
282
+ .delete()
283
+ .eq('node_id', nodeId)
284
+ .eq('user_id', getUserId());
285
+ return !error;
286
+ }
287
+ // --- Search ---
288
+ export async function searchNodes(query, filters) {
289
+ let q = supabase
290
+ .from('nodes')
291
+ .select('*')
292
+ .or(`title.ilike.%${query}%,description.ilike.%${query}%`)
293
+ .order('sort_order', { ascending: true })
294
+ .limit(50);
295
+ if (filters?.type && filters.type.length > 0) {
296
+ q = q.in('type', filters.type);
297
+ }
298
+ if (filters?.status && filters.status.length > 0) {
299
+ q = q.in('status', filters.status);
300
+ }
301
+ const { data: rows, error } = await q;
302
+ if (error)
303
+ throw new Error(error.message);
304
+ return (rows || []).map(row => dbNodeToTree(row, [], []));
305
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,193 @@
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 { authenticate, supabase } from './supabase.js';
6
+ import { getTree, getNode, insertNode, updateNode, deleteNode, moveNode, searchNodes, getWorkMode } from './db.js';
7
+ const server = new McpServer({
8
+ name: 'slyplan',
9
+ version: '1.0.0',
10
+ });
11
+ // --- Session state ---
12
+ let activeProjectId = null;
13
+ async function getActiveProjectName() {
14
+ if (!activeProjectId)
15
+ return '(none)';
16
+ const node = await getNode(activeProjectId);
17
+ return node ? node.title : '(unknown)';
18
+ }
19
+ function resolveParentId(parentId) {
20
+ if (parentId !== undefined && parentId !== null)
21
+ return parentId;
22
+ return activeProjectId;
23
+ }
24
+ // --- Tools ---
25
+ server.tool('list_projects', 'List all projects. Use this first to see available projects.', {}, async () => {
26
+ const tree = await getTree(null);
27
+ const projects = tree.filter(n => n.type === 'project');
28
+ const lines = projects.map(p => {
29
+ const active = p.id === activeProjectId ? ' ← ACTIVE' : '';
30
+ return `• ${p.title} (${p.status}, ${p.progress}%) [${p.id}]${active}`;
31
+ });
32
+ const header = activeProjectId
33
+ ? `Active project: ${await getActiveProjectName()}`
34
+ : 'No active project selected. Use set_project to choose one.';
35
+ return {
36
+ content: [{ type: 'text', text: `${header}\n\nProjects:\n${lines.join('\n') || '(no projects)'}` }],
37
+ };
38
+ });
39
+ server.tool('set_project', 'Select which project to work in. All subsequent operations (add_node, get_tree, search) will apply to this project.', {
40
+ project_id: z.string().describe('Project ID to work in. Get from list_projects.'),
41
+ }, async ({ project_id }) => {
42
+ const node = await getNode(project_id);
43
+ if (!node)
44
+ return { content: [{ type: 'text', text: 'Project not found.' }], isError: true };
45
+ if (node.type !== 'project')
46
+ return { content: [{ type: 'text', text: `"${node.title}" is a ${node.type}, not a project.` }], isError: true };
47
+ activeProjectId = project_id;
48
+ return {
49
+ content: [{ type: 'text', text: `Active project set to: "${node.title}"\n\nAll add_node, get_tree and search operations will now apply to this project.\nUse get_tree to see the project structure.` }],
50
+ };
51
+ });
52
+ server.tool('get_tree', 'Get the project tree. Shows the active project if set, or the full tree.', { root_id: z.string().optional().describe('Node ID to use as root. Omit for active project / full tree.') }, async ({ root_id }) => {
53
+ const effectiveRoot = root_id ?? activeProjectId ?? null;
54
+ const tree = await getTree(effectiveRoot);
55
+ const ctx = activeProjectId ? `Project: ${await getActiveProjectName()}\n\n` : '';
56
+ return { content: [{ type: 'text', text: `${ctx}${JSON.stringify(tree, null, 2)}` }] };
57
+ });
58
+ server.tool('get_node', 'Get details about a single node', { id: z.string().describe('Node ID to fetch') }, async ({ id }) => {
59
+ const node = await getNode(id);
60
+ if (!node)
61
+ return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
62
+ return { content: [{ type: 'text', text: JSON.stringify(node, null, 2) }] };
63
+ });
64
+ server.tool('add_node', 'Add a new node. If parent_id is not set, the node is added under the active project.', {
65
+ parent_id: z.string().nullable().optional().describe('Parent ID. Omit to use active project as parent.'),
66
+ type: z.enum(['project', 'category', 'module', 'component', 'feature', 'task']).describe('Node type'),
67
+ title: z.string().describe('Node title'),
68
+ description: z.string().optional().describe('Description'),
69
+ status: z.enum(['not_started', 'in_progress', 'blocked', 'done']).optional().describe('Status'),
70
+ progress: z.number().min(0).max(100).optional().describe('Progress percentage (0-100)'),
71
+ metadata: z.record(z.unknown()).optional().describe('Extra metadata as JSON'),
72
+ }, async ({ parent_id, type, title, description, status, progress, metadata }) => {
73
+ try {
74
+ const effectiveParent = type === 'project' ? null : resolveParentId(parent_id);
75
+ const node = await insertNode({
76
+ parentId: effectiveParent,
77
+ type, title, description, status, progress, metadata,
78
+ });
79
+ const parentNode = effectiveParent ? await getNode(effectiveParent) : null;
80
+ return { content: [{ type: 'text', text: `Node created under ${parentNode ? `"${parentNode.title}"` : 'root'}:\n${JSON.stringify(node, null, 2)}` }] };
81
+ }
82
+ catch (e) {
83
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
84
+ }
85
+ });
86
+ server.tool('update_node', 'Update fields on an existing node', {
87
+ id: z.string().describe('Node ID to update'),
88
+ title: z.string().optional().describe('New title'),
89
+ description: z.string().optional().describe('New description'),
90
+ status: z.enum(['not_started', 'in_progress', 'blocked', 'done']).optional().describe('New status'),
91
+ progress: z.number().min(0).max(100).optional().describe('New progress (0-100)'),
92
+ metadata: z.record(z.unknown()).optional().describe('New metadata'),
93
+ }, async ({ id, ...updates }) => {
94
+ const node = await updateNode(id, updates);
95
+ if (!node)
96
+ return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
97
+ return { content: [{ type: 'text', text: `Node updated:\n${JSON.stringify(node, null, 2)}` }] };
98
+ });
99
+ server.tool('move_node', 'Move a node to a new parent', {
100
+ id: z.string().describe('Node ID to move'),
101
+ new_parent_id: z.string().nullable().describe('New parent ID (null for root level)'),
102
+ sort_order: z.number().optional().describe('Position among siblings'),
103
+ }, async ({ id, new_parent_id, sort_order }) => {
104
+ const node = await moveNode(id, new_parent_id, sort_order);
105
+ if (!node)
106
+ return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
107
+ return { content: [{ type: 'text', text: `Node moved:\n${JSON.stringify(node, null, 2)}` }] };
108
+ });
109
+ server.tool('delete_node', 'Delete a node and all its children', {
110
+ id: z.string().describe('Node ID to delete'),
111
+ }, async ({ id }) => {
112
+ const node = await getNode(id);
113
+ if (!node)
114
+ return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
115
+ await deleteNode(id);
116
+ return { content: [{ type: 'text', text: `Deleted node "${node.title}" (${id}) and all children.` }] };
117
+ });
118
+ server.tool('search', 'Search for nodes by text, type and status. Searches in active project if set.', {
119
+ query: z.string().describe('Search term for title/description'),
120
+ type: z.enum(['project', 'category', 'module', 'component', 'feature', 'task']).optional().describe('Filter by type'),
121
+ status: z.enum(['not_started', 'in_progress', 'blocked', 'done']).optional().describe('Filter by status'),
122
+ }, async ({ query, type, status }) => {
123
+ const results = await searchNodes(query, {
124
+ type: type ? [type] : undefined,
125
+ status: status ? [status] : undefined,
126
+ });
127
+ return {
128
+ content: [{
129
+ type: 'text',
130
+ text: results.length > 0
131
+ ? `Found ${results.length} results:\n${JSON.stringify(results, null, 2)}`
132
+ : 'No results found.',
133
+ }],
134
+ };
135
+ });
136
+ server.tool('get_work_mode', 'Get all nodes currently in Work Mode', {}, async () => {
137
+ const items = await getWorkMode();
138
+ return {
139
+ content: [{
140
+ type: 'text',
141
+ text: items.length > 0
142
+ ? `Work Mode (${items.length} active):\n${JSON.stringify(items, null, 2)}`
143
+ : 'No nodes in Work Mode.',
144
+ }],
145
+ };
146
+ });
147
+ server.tool('request_completion', 'Analyze a node and suggest completion based on children status', {
148
+ id: z.string().describe('Node ID to analyze'),
149
+ context: z.string().optional().describe('Extra context about what was done'),
150
+ }, async ({ id, context }) => {
151
+ const node = await getNode(id);
152
+ if (!node)
153
+ return { content: [{ type: 'text', text: 'Node not found' }], isError: true };
154
+ const children = node.children;
155
+ const summary = {
156
+ node: { id: node.id, title: node.title, type: node.type, status: node.status, progress: node.progress },
157
+ children_summary: {
158
+ total: children.length,
159
+ done: children.filter(c => c.status === 'done').length,
160
+ in_progress: children.filter(c => c.status === 'in_progress').length,
161
+ blocked: children.filter(c => c.status === 'blocked').length,
162
+ not_started: children.filter(c => c.status === 'not_started').length,
163
+ },
164
+ suggested_progress: children.length > 0
165
+ ? Math.round(children.reduce((sum, c) => sum + c.progress, 0) / children.length)
166
+ : node.progress,
167
+ context: context || 'No extra context',
168
+ };
169
+ const allDone = children.length > 0 && children.every(c => c.status === 'done');
170
+ return {
171
+ content: [{
172
+ type: 'text',
173
+ text: allDone
174
+ ? `All ${children.length} children are done! Node "${node.title}" can be marked as done.\n\n${JSON.stringify(summary, null, 2)}`
175
+ : `Analysis of "${node.title}":\n${JSON.stringify(summary, null, 2)}`,
176
+ }],
177
+ };
178
+ });
179
+ server.tool('whoami', 'Show which user is authenticated for this MCP session.', {}, async () => {
180
+ const { data: { user } } = await supabase.auth.getUser();
181
+ if (!user)
182
+ return { content: [{ type: 'text', text: 'Not authenticated.' }], isError: true };
183
+ return {
184
+ content: [{ type: 'text', text: `Authenticated as: ${user.email} (${user.id})` }],
185
+ };
186
+ });
187
+ // --- Start ---
188
+ async function main() {
189
+ await authenticate();
190
+ const transport = new StdioServerTransport();
191
+ await server.connect(transport);
192
+ }
193
+ main().catch(console.error);
@@ -0,0 +1,3 @@
1
+ export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
2
+ export declare function authenticate(): Promise<void>;
3
+ export declare function getUserId(): string;
@@ -0,0 +1,27 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ const SUPABASE_URL = 'https://omfzpkwtuzucwwxmyuqt.supabase.co';
3
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9tZnpwa3d0dXp1Y3d3eG15dXF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5MjMwNDIsImV4cCI6MjA4NjQ5OTA0Mn0.KXGoUez7M45RtFM9qR7mjzGX6UhlaRE-gggAJxSkIHY';
4
+ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
5
+ auth: { autoRefreshToken: true, persistSession: false },
6
+ });
7
+ let userId = null;
8
+ export async function authenticate() {
9
+ const email = process.env.SLYPLAN_EMAIL;
10
+ const password = process.env.SLYPLAN_PASSWORD;
11
+ if (!email || !password) {
12
+ console.error('Missing SLYPLAN_EMAIL or SLYPLAN_PASSWORD environment variables.');
13
+ console.error('Set them in your Claude MCP config under "env".');
14
+ process.exit(1);
15
+ }
16
+ const { data, error } = await supabase.auth.signInWithPassword({ email, password });
17
+ if (error) {
18
+ console.error(`Authentication failed: ${error.message}`);
19
+ process.exit(1);
20
+ }
21
+ userId = data.user.id;
22
+ }
23
+ export function getUserId() {
24
+ if (!userId)
25
+ throw new Error('Not authenticated. Call authenticate() first.');
26
+ return userId;
27
+ }
@@ -0,0 +1,26 @@
1
+ export type NodeType = 'project' | 'category' | 'module' | 'component' | 'feature' | 'task';
2
+ export type Status = 'not_started' | 'in_progress' | 'blocked' | 'done';
3
+ export interface TreeNode {
4
+ id: string;
5
+ parentId: string | null;
6
+ type: NodeType;
7
+ title: string;
8
+ description: string;
9
+ status: Status;
10
+ progress: number;
11
+ sortOrder: number;
12
+ collapsed: boolean;
13
+ metadata: Record<string, unknown>;
14
+ links: Link[];
15
+ children: TreeNode[];
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ }
19
+ export interface Link {
20
+ id: string;
21
+ nodeId: string;
22
+ url: string;
23
+ label: string;
24
+ linkType: string;
25
+ createdAt: string;
26
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "slyplan-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Slyplan — visual project management via Claude",
5
+ "type": "module",
6
+ "bin": {
7
+ "slyplan-mcp": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "slyplan",
19
+ "project-management",
20
+ "claude",
21
+ "ai"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.26.0",
26
+ "@supabase/supabase-js": "^2.78.0",
27
+ "uuid": "^11.0.5",
28
+ "zod": "^3.25.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/uuid": "^10.0.0",
32
+ "typescript": "^5.7.3"
33
+ }
34
+ }