slyplan-mcp 1.4.1 → 1.5.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/cli.js CHANGED
@@ -3,11 +3,11 @@ import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as http from 'http';
5
5
  import * as readline from 'readline';
6
- import { createClient } from '@supabase/supabase-js';
7
6
  import { exec } from 'child_process';
7
+ import { createClient } from '@supabase/supabase-js';
8
8
  const SUPABASE_URL = 'https://omfzpkwtuzucwwxmyuqt.supabase.co';
9
9
  const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9tZnpwa3d0dXp1Y3d3eG15dXF0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5MjMwNDIsImV4cCI6MjA4NjQ5OTA0Mn0.KXGoUez7M45RtFM9qR7mjzGX6UhlaRE-gggAJxSkIHY';
10
- const SLYPLAN_URL = 'https://slyplan.com';
10
+ const SLYPLAN_ORIGIN = 'https://slyplan.com';
11
11
  // Smart transcript-aware hook: checks if set_project + add_to_work_mode have been called.
12
12
  // Silent when OK, reminds only when needed.
13
13
  const PRE_HOOK_FILE_CONTENT = `#!/usr/bin/env node
@@ -218,136 +218,66 @@ You MUST keep SlyPlan updated. This is not optional. Follow these rules:
218
218
  function log(msg) {
219
219
  process.stderr.write(msg + '\n');
220
220
  }
221
- function promptUser(question, hidden = false) {
221
+ function promptUser(question) {
222
222
  return new Promise((resolve) => {
223
- if (hidden && process.stdin.isTTY) {
224
- // Password masking via raw mode
225
- process.stderr.write(question);
226
- const stdin = process.stdin;
227
- stdin.setRawMode(true);
228
- stdin.resume();
229
- stdin.setEncoding('utf8');
230
- let password = '';
231
- const onData = (ch) => {
232
- const c = ch.toString();
233
- if (c === '\n' || c === '\r' || c === '\u0004') {
234
- stdin.setRawMode(false);
235
- stdin.pause();
236
- stdin.removeListener('data', onData);
237
- process.stderr.write('\n');
238
- resolve(password);
239
- }
240
- else if (c === '\u0003') {
241
- // Ctrl+C
242
- stdin.setRawMode(false);
243
- process.exit(1);
244
- }
245
- else if (c === '\u007f' || c === '\b') {
246
- // Backspace
247
- if (password.length > 0) {
248
- password = password.slice(0, -1);
249
- process.stderr.write('\b \b');
250
- }
251
- }
252
- else {
253
- password += c;
254
- process.stderr.write('*');
255
- }
256
- };
257
- stdin.on('data', onData);
258
- }
259
- else {
260
- // Non-TTY or visible input
261
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
262
- rl.question(question, (answer) => {
263
- rl.close();
264
- resolve(answer.trim());
265
- });
266
- }
267
- });
268
- }
269
- async function validateCredentials(email, password) {
270
- const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
271
- auth: { autoRefreshToken: false, persistSession: false },
223
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
224
+ rl.question(question, (answer) => {
225
+ rl.close();
226
+ resolve(answer.trim());
227
+ });
272
228
  });
273
- const { data, error } = await client.auth.signInWithPassword({ email, password });
274
- if (error)
275
- return { ok: false, error: error.message };
276
- return { ok: true, userEmail: data.user?.email ?? email, refreshToken: data.session?.refresh_token };
277
- }
278
- function readJsonFile(filePath) {
279
- try {
280
- const content = fs.readFileSync(filePath, 'utf8');
281
- return JSON.parse(content);
282
- }
283
- catch {
284
- return null;
285
- }
286
- }
287
- function writeJsonFile(filePath, data) {
288
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
289
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
290
229
  }
291
- // --- Browser Auth ---
292
230
  function openBrowser(url) {
293
231
  const platform = process.platform;
294
- const cmd = platform === 'win32' ? 'start ""'
295
- : platform === 'darwin' ? 'open'
296
- : 'xdg-open';
297
- exec(`${cmd} "${url}"`);
232
+ const cmd = platform === 'win32' ? `start "" "${url}"`
233
+ : platform === 'darwin' ? `open "${url}"`
234
+ : `xdg-open "${url}"`;
235
+ exec(cmd, () => {
236
+ // Ignore errors — user can always open manually
237
+ });
298
238
  }
299
239
  const SUCCESS_HTML = `<!DOCTYPE html>
300
- <html lang="en">
301
- <head><meta charset="utf-8"><title>SlyPlan - Authorized</title>
240
+ <html>
241
+ <head><title>SlyPlan Authorized</title>
302
242
  <style>
303
- body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
304
- background: #09090b; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
305
- .card { text-align: center; padding: 3rem; }
306
- .check { width: 64px; height: 64px; margin: 0 auto 1.5rem; color: #22c55e; }
307
- h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
308
- p { color: #a1a1aa; font-size: 0.875rem; margin: 0; }
309
- </style>
310
- </head>
311
- <body>
312
- <div class="card">
313
- <svg class="check" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
314
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
315
- </svg>
243
+ body { background: #09090b; color: #e4e4e7; font-family: system-ui, sans-serif;
244
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
245
+ .card { text-align: center; padding: 3rem; border-radius: 12px;
246
+ background: #18181b; border: 1px solid #27272a; max-width: 360px; }
247
+ .check { color: #22c55e; font-size: 3rem; margin-bottom: 1rem; }
248
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
249
+ p { color: #71717a; font-size: 0.875rem; margin: 0; }
250
+ </style></head>
251
+ <body><div class="card">
252
+ <div class="check">&#10003;</div>
316
253
  <h1>Authorized!</h1>
317
254
  <p>You can close this tab and return to your terminal.</p>
318
- </div>
319
- </body>
320
- </html>`;
255
+ </div></body></html>`;
321
256
  const ERROR_HTML = (msg) => `<!DOCTYPE html>
322
- <html lang="en">
323
- <head><meta charset="utf-8"><title>SlyPlan - Error</title>
257
+ <html>
258
+ <head><title>SlyPlan Error</title>
324
259
  <style>
325
- body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
326
- background: #09090b; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
327
- .card { text-align: center; padding: 3rem; }
328
- .icon { width: 64px; height: 64px; margin: 0 auto 1.5rem; color: #ef4444; }
329
- h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
330
- p { color: #a1a1aa; font-size: 0.875rem; margin: 0; }
331
- </style>
332
- </head>
333
- <body>
334
- <div class="card">
335
- <svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
336
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
337
- </svg>
260
+ body { background: #09090b; color: #e4e4e7; font-family: system-ui, sans-serif;
261
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
262
+ .card { text-align: center; padding: 3rem; border-radius: 12px;
263
+ background: #18181b; border: 1px solid #27272a; max-width: 360px; }
264
+ .icon { color: #ef4444; font-size: 3rem; margin-bottom: 1rem; }
265
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
266
+ p { color: #71717a; font-size: 0.875rem; margin: 0; }
267
+ </style></head>
268
+ <body><div class="card">
269
+ <div class="icon">&#10007;</div>
338
270
  <h1>Authorization Failed</h1>
339
271
  <p>${msg}</p>
340
- </div>
341
- </body>
342
- </html>`;
272
+ </div></body></html>`;
343
273
  function startLocalAuthServer() {
344
274
  return new Promise((resolve, reject) => {
345
275
  const server = http.createServer((req, res) => {
346
276
  const url = new URL(req.url ?? '/', `http://localhost`);
347
277
  if (url.pathname === '/callback') {
348
278
  const refreshToken = url.searchParams.get('refresh_token');
349
- const email = url.searchParams.get('email') ?? '';
350
- if (refreshToken) {
279
+ const email = url.searchParams.get('email');
280
+ if (refreshToken && email) {
351
281
  res.writeHead(200, { 'Content-Type': 'text/html' });
352
282
  res.end(SUCCESS_HTML);
353
283
  server.close();
@@ -355,45 +285,67 @@ function startLocalAuthServer() {
355
285
  }
356
286
  else {
357
287
  res.writeHead(400, { 'Content-Type': 'text/html' });
358
- res.end(ERROR_HTML('Missing refresh token in callback.'));
288
+ res.end(ERROR_HTML('Missing token or email in callback.'));
359
289
  server.close();
360
- reject(new Error('Missing refresh token in callback'));
290
+ reject(new Error('Missing token or email in callback'));
361
291
  }
362
292
  }
363
293
  else {
364
- res.writeHead(404, { 'Content-Type': 'text/plain' });
365
- res.end('Not found');
294
+ res.writeHead(404);
295
+ res.end();
366
296
  }
367
297
  });
368
- // Listen on random port
369
298
  server.listen(0, '127.0.0.1', () => {
370
299
  const addr = server.address();
371
300
  if (!addr || typeof addr === 'string') {
372
- reject(new Error('Failed to start local server'));
301
+ reject(new Error('Failed to get server address'));
373
302
  return;
374
303
  }
375
304
  const port = addr.port;
376
305
  const callbackUrl = `http://localhost:${port}/callback`;
377
- const authUrl = `${SLYPLAN_URL}/cli/auth?port=${port}&callback=${encodeURIComponent(callbackUrl)}`;
378
- log(` Opening browser...`);
379
- log(` If it doesn't open, visit:`);
380
- log(` ${authUrl}`);
306
+ const authUrl = `${SLYPLAN_ORIGIN}/cli/auth?port=${port}&callback=${encodeURIComponent(callbackUrl)}`;
307
+ log(` Opening browser to authorize...`);
308
+ log(` If it doesn't open, visit: ${authUrl}`);
381
309
  log('');
382
310
  openBrowser(authUrl);
383
311
  });
384
312
  // Timeout after 120 seconds
385
313
  setTimeout(() => {
386
314
  server.close();
387
- reject(new Error('Authorization timed out after 120 seconds. Please try again.'));
315
+ reject(new Error('Authorization timed out (120s). Try again.'));
388
316
  }, 120000);
389
317
  });
390
318
  }
319
+ async function validateCredentials(email, password) {
320
+ const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
321
+ auth: { autoRefreshToken: false, persistSession: false },
322
+ });
323
+ const { data, error } = await client.auth.signInWithPassword({ email, password });
324
+ if (error)
325
+ return { ok: false, error: error.message };
326
+ return {
327
+ ok: true,
328
+ userEmail: data.user?.email ?? email,
329
+ refreshToken: data.session?.refresh_token,
330
+ };
331
+ }
332
+ function readJsonFile(filePath) {
333
+ try {
334
+ const content = fs.readFileSync(filePath, 'utf8');
335
+ return JSON.parse(content);
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
341
+ function writeJsonFile(filePath, data) {
342
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
343
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
344
+ }
391
345
  // --- Settings Merge ---
392
- // MCP server + env go in settings.local.json (user-local, not committed)
393
- // Hooks go in settings.json (can be committed)
394
- function mergeLocalSettings(existing, refreshToken) {
346
+ function mergeSettings(existing, refreshToken) {
395
347
  const result = { ...existing };
396
- // MCP server config with credentials
348
+ // MCP server config
397
349
  if (!result.mcpServers)
398
350
  result.mcpServers = {};
399
351
  result.mcpServers.slyplan = {
@@ -403,10 +355,6 @@ function mergeLocalSettings(existing, refreshToken) {
403
355
  SLYPLAN_REFRESH_TOKEN: refreshToken,
404
356
  },
405
357
  };
406
- return result;
407
- }
408
- function mergeSettings(existing) {
409
- const result = { ...existing };
410
358
  // Hook config
411
359
  if (!result.hooks)
412
360
  result.hooks = {};
@@ -450,18 +398,9 @@ function mergeSettings(existing) {
450
398
  }
451
399
  return result;
452
400
  }
453
- function removeFromLocalSettings(existing) {
454
- const result = { ...existing };
455
- if (result.mcpServers) {
456
- delete result.mcpServers.slyplan;
457
- if (Object.keys(result.mcpServers).length === 0)
458
- delete result.mcpServers;
459
- }
460
- return result;
461
- }
462
401
  function removeFromSettings(existing) {
463
402
  const result = { ...existing };
464
- // Remove MCP server (legacy — was previously in settings.json)
403
+ // Remove MCP server
465
404
  if (result.mcpServers) {
466
405
  delete result.mcpServers.slyplan;
467
406
  if (Object.keys(result.mcpServers).length === 0)
@@ -488,7 +427,6 @@ async function runSetup() {
488
427
  const claudeDir = path.join(cwd, '.claude');
489
428
  const hooksDir = path.join(claudeDir, 'hooks');
490
429
  const settingsPath = path.join(claudeDir, 'settings.json');
491
- const localSettingsPath = path.join(claudeDir, 'settings.local.json');
492
430
  const postHookFilePath = path.join(hooksDir, 'slyplan-post-tool-sync.cjs');
493
431
  const preHookFilePath = path.join(hooksDir, 'slyplan-pre-tool-sync.cjs');
494
432
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
@@ -497,7 +435,7 @@ async function runSetup() {
497
435
  log(' ==============================');
498
436
  log(` Setting up in: ${cwd}`);
499
437
  log('');
500
- // Parse flags
438
+ // Parse flags (--email + --password for CI/scripted use)
501
439
  const args = process.argv.slice(3);
502
440
  let flagEmail = '';
503
441
  let flagPassword = '';
@@ -510,7 +448,7 @@ async function runSetup() {
510
448
  let refreshToken = '';
511
449
  let userEmail = '';
512
450
  if (flagEmail && flagPassword) {
513
- // CI/scripted mode: validate credentials and extract refresh token
451
+ // CI mode: validate credentials and extract refresh token
514
452
  process.stderr.write(' Validating credentials... ');
515
453
  const result = await validateCredentials(flagEmail, flagPassword);
516
454
  if (!result.ok) {
@@ -527,8 +465,6 @@ async function runSetup() {
527
465
  }
528
466
  else {
529
467
  // Interactive mode: browser-based auth
530
- log(' Waiting for authorization in browser...');
531
- log('');
532
468
  try {
533
469
  const result = await startLocalAuthServer();
534
470
  refreshToken = result.refreshToken;
@@ -536,7 +472,6 @@ async function runSetup() {
536
472
  log(` Authorized as ${userEmail}`);
537
473
  }
538
474
  catch (err) {
539
- log('');
540
475
  log(` ${err.message}`);
541
476
  process.exit(1);
542
477
  }
@@ -548,18 +483,11 @@ async function runSetup() {
548
483
  log(' [+] Created .claude/hooks/slyplan-pre-tool-sync.cjs');
549
484
  fs.writeFileSync(postHookFilePath, POST_HOOK_FILE_CONTENT, 'utf8');
550
485
  log(' [+] Created .claude/hooks/slyplan-post-tool-sync.cjs');
551
- // 2a. Merge settings.local.json (MCP server + credentials)
552
- let existingLocal = {};
553
- const rawLocal = readJsonFile(localSettingsPath);
554
- if (rawLocal)
555
- existingLocal = rawLocal;
556
- const mergedLocal = mergeLocalSettings(existingLocal, refreshToken);
557
- writeJsonFile(localSettingsPath, mergedLocal);
558
- log(' [+] Updated .claude/settings.local.json (MCP server)');
559
- // 2b. Merge settings.json (hooks — safe to commit)
486
+ // 2. Merge settings.json
560
487
  let existingSettings = {};
561
488
  const rawSettings = readJsonFile(settingsPath);
562
489
  if (rawSettings === null && fs.existsSync(settingsPath)) {
490
+ // File exists but is malformed
563
491
  const answer = await promptUser(' .claude/settings.json exists but is malformed. Overwrite? [y/N]: ');
564
492
  if (answer.toLowerCase() !== 'y') {
565
493
  log(' Aborting. Fix settings.json manually and re-run setup.');
@@ -569,9 +497,9 @@ async function runSetup() {
569
497
  else if (rawSettings) {
570
498
  existingSettings = rawSettings;
571
499
  }
572
- const mergedSettings = mergeSettings(existingSettings);
500
+ const mergedSettings = mergeSettings(existingSettings, refreshToken);
573
501
  writeJsonFile(settingsPath, mergedSettings);
574
- log(' [+] Updated .claude/settings.json (hooks)');
502
+ log(' [+] Updated .claude/settings.json');
575
503
  // 3. Optional CLAUDE.md append
576
504
  const appendAnswer = await promptUser(' Append SlyPlan sync instructions to CLAUDE.md? [Y/n]: ');
577
505
  if (appendAnswer.toLowerCase() !== 'n') {
@@ -608,7 +536,6 @@ async function runRemove() {
608
536
  const cwd = process.cwd();
609
537
  const claudeDir = path.join(cwd, '.claude');
610
538
  const settingsPath = path.join(claudeDir, 'settings.json');
611
- const localSettingsPath = path.join(claudeDir, 'settings.local.json');
612
539
  const postHookFilePath = path.join(claudeDir, 'hooks', 'slyplan-post-tool-sync.cjs');
613
540
  const preHookFilePath = path.join(claudeDir, 'hooks', 'slyplan-pre-tool-sync.cjs');
614
541
  log('');
@@ -629,20 +556,7 @@ async function runRemove() {
629
556
  log(` [=] .claude/hooks/${name} not found — skipped`);
630
557
  }
631
558
  }
632
- // 2a. Clean settings.local.json (MCP server)
633
- const existingLocal = readJsonFile(localSettingsPath);
634
- if (existingLocal) {
635
- const cleanedLocal = removeFromLocalSettings(existingLocal);
636
- if (Object.keys(cleanedLocal).length === 0) {
637
- fs.unlinkSync(localSettingsPath);
638
- log(' [-] Deleted empty .claude/settings.local.json');
639
- }
640
- else {
641
- writeJsonFile(localSettingsPath, cleanedLocal);
642
- log(' [-] Removed SlyPlan from .claude/settings.local.json');
643
- }
644
- }
645
- // 2b. Clean settings.json (hooks + legacy MCP server)
559
+ // 2. Clean settings.json
646
560
  const existing = readJsonFile(settingsPath);
647
561
  if (existing) {
648
562
  const cleaned = removeFromSettings(existing);
package/dist/supabase.js CHANGED
@@ -7,54 +7,34 @@ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
7
7
  auth: { autoRefreshToken: true, persistSession: false },
8
8
  });
9
9
  let userId = null;
10
- function findSettingsPath() {
11
- // Walk up from cwd looking for .claude/settings.local.json (preferred) or settings.json
12
- let dir = process.cwd();
13
- while (true) {
14
- const localCandidate = path.join(dir, '.claude', 'settings.local.json');
15
- if (fs.existsSync(localCandidate))
16
- return localCandidate;
17
- const candidate = path.join(dir, '.claude', 'settings.json');
18
- if (fs.existsSync(candidate))
19
- return candidate;
20
- const parent = path.dirname(dir);
21
- if (parent === dir)
22
- break;
23
- dir = parent;
24
- }
25
- return null;
26
- }
27
10
  function persistNewRefreshToken(newToken) {
11
+ const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
28
12
  try {
29
- const settingsPath = findSettingsPath();
30
- if (!settingsPath)
31
- return;
32
- const raw = fs.readFileSync(settingsPath, 'utf8');
33
- const settings = JSON.parse(raw);
34
- const env = settings?.mcpServers?.slyplan?.env;
35
- if (!env)
36
- return;
37
- env.SLYPLAN_REFRESH_TOKEN = newToken;
38
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
13
+ const content = fs.readFileSync(settingsPath, 'utf8');
14
+ const settings = JSON.parse(content);
15
+ if (settings?.mcpServers?.slyplan?.env) {
16
+ settings.mcpServers.slyplan.env.SLYPLAN_REFRESH_TOKEN = newToken;
17
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
18
+ }
39
19
  }
40
20
  catch {
41
- // Silenttoken rotation is best-effort
21
+ // Best-effortdon't crash if settings file is missing/malformed
42
22
  }
43
23
  }
44
24
  export async function authenticate() {
45
25
  const refreshToken = process.env.SLYPLAN_REFRESH_TOKEN;
46
26
  const email = process.env.SLYPLAN_EMAIL;
47
27
  const password = process.env.SLYPLAN_PASSWORD;
48
- // Prefer refresh token (browser auth flow)
28
+ // Prefer refresh token (new browser-based flow)
49
29
  if (refreshToken) {
50
30
  const { data, error } = await supabase.auth.refreshSession({ refresh_token: refreshToken });
51
31
  if (error) {
52
32
  console.error(`Token refresh failed: ${error.message}`);
53
- console.error('Re-run "npx slyplan-mcp setup" to re-authorize.');
33
+ console.error('Run "npx slyplan-mcp setup" to re-authenticate.');
54
34
  process.exit(1);
55
35
  }
56
36
  userId = data.user.id;
57
- // Persist rotated token for next startup
37
+ // Self-healing: persist rotated token
58
38
  if (data.session?.refresh_token && data.session.refresh_token !== refreshToken) {
59
39
  persistNewRefreshToken(data.session.refresh_token);
60
40
  }
@@ -70,8 +50,8 @@ export async function authenticate() {
70
50
  userId = data.user.id;
71
51
  return;
72
52
  }
73
- console.error('Missing SLYPLAN_REFRESH_TOKEN (or legacy SLYPLAN_EMAIL + SLYPLAN_PASSWORD).');
74
- console.error('Run "npx slyplan-mcp setup" to authorize.');
53
+ console.error('Missing SLYPLAN_REFRESH_TOKEN (or SLYPLAN_EMAIL + SLYPLAN_PASSWORD).');
54
+ console.error('Run "npx slyplan-mcp setup" to configure authentication.');
75
55
  process.exit(1);
76
56
  }
77
57
  export function getUserId() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slyplan-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for Slyplan — visual project management via Claude",
5
5
  "type": "module",
6
6
  "bin": {