shsu 0.0.1 → 0.0.5

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.
@@ -8,12 +8,13 @@ on:
8
8
  jobs:
9
9
  publish:
10
10
  runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write
11
14
  steps:
12
15
  - uses: actions/checkout@v4
13
16
  - uses: actions/setup-node@v4
14
17
  with:
15
- node-version: '20'
18
+ node-version: '24'
16
19
  registry-url: 'https://registry.npmjs.org'
17
- - run: npm publish
18
- env:
19
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
20
+ - run: npm publish --provenance --access public
package/README.md CHANGED
@@ -86,6 +86,74 @@ Config is read from `package.json` "shsu" key. Environment variables override pa
86
86
  | `url` / `SHSU_URL` | For `invoke` | Supabase URL |
87
87
  | `localPath` / `SHSU_LOCAL_PATH` | No | Local functions path (default: `./supabase/functions`) |
88
88
 
89
+ ## MCP Server
90
+
91
+ shsu can run as an MCP server for AI assistants.
92
+
93
+ ### Claude Code
94
+
95
+ Add to `.mcp.json` in your project root:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "shsu": {
101
+ "command": "npx",
102
+ "args": ["shsu", "mcp"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Claude Desktop
109
+
110
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "shsu": {
116
+ "command": "npx",
117
+ "args": ["shsu", "mcp"],
118
+ "cwd": "/path/to/your/project"
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Cursor
125
+
126
+ Add to `.cursor/mcp.json` in your project:
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "shsu": {
132
+ "command": "npx",
133
+ "args": ["shsu", "mcp"]
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### Available Tools
140
+
141
+ - `deploy` - Deploy edge functions
142
+ - `list` - List local and remote functions
143
+ - `invoke` - Invoke a function
144
+ - `restart` - Restart edge-runtime
145
+ - `new` - Create new function from template
146
+ - `config` - Show current configuration
147
+
148
+ ## Releasing
149
+
150
+ ```bash
151
+ npm version patch # or minor/major
152
+ git push --follow-tags
153
+ ```
154
+
155
+ GitHub Actions will automatically publish to npm when the tag is pushed.
156
+
89
157
  ## License
90
158
 
91
159
  MIT
package/bin/shsu.mjs CHANGED
@@ -308,6 +308,242 @@ async function cmdInit() {
308
308
  success('Added shsu config to package.json');
309
309
  }
310
310
 
311
+ // ─────────────────────────────────────────────────────────────
312
+ // MCP Server
313
+ // ─────────────────────────────────────────────────────────────
314
+ async function cmdMcp() {
315
+ const tools = [
316
+ {
317
+ name: 'deploy',
318
+ description: 'Deploy edge function(s) to the server. Syncs via rsync and restarts edge-runtime.',
319
+ inputSchema: {
320
+ type: 'object',
321
+ properties: {
322
+ name: { type: 'string', description: 'Function name to deploy. If omitted, deploys all functions.' },
323
+ noRestart: { type: 'boolean', description: 'Skip restarting edge-runtime after deploy.' },
324
+ },
325
+ },
326
+ },
327
+ {
328
+ name: 'list',
329
+ description: 'List edge functions (both local and remote).',
330
+ inputSchema: { type: 'object', properties: {} },
331
+ },
332
+ {
333
+ name: 'invoke',
334
+ description: 'Invoke an edge function with optional JSON data.',
335
+ inputSchema: {
336
+ type: 'object',
337
+ properties: {
338
+ name: { type: 'string', description: 'Function name to invoke.' },
339
+ data: { type: 'string', description: 'JSON data to send (default: {}).' },
340
+ },
341
+ required: ['name'],
342
+ },
343
+ },
344
+ {
345
+ name: 'restart',
346
+ description: 'Restart the edge-runtime container.',
347
+ inputSchema: { type: 'object', properties: {} },
348
+ },
349
+ {
350
+ name: 'new',
351
+ description: 'Create a new edge function from template.',
352
+ inputSchema: {
353
+ type: 'object',
354
+ properties: {
355
+ name: { type: 'string', description: 'Name for the new function.' },
356
+ },
357
+ required: ['name'],
358
+ },
359
+ },
360
+ {
361
+ name: 'config',
362
+ description: 'Get current shsu configuration.',
363
+ inputSchema: { type: 'object', properties: {} },
364
+ },
365
+ ];
366
+
367
+ const serverInfo = {
368
+ name: 'shsu',
369
+ version: '0.0.1',
370
+ };
371
+
372
+ // Helper to write JSON-RPC response
373
+ const respond = (id, result) => {
374
+ const response = { jsonrpc: '2.0', id, result };
375
+ process.stdout.write(JSON.stringify(response) + '\n');
376
+ };
377
+
378
+ const respondError = (id, code, message) => {
379
+ const response = { jsonrpc: '2.0', id, error: { code, message } };
380
+ process.stdout.write(JSON.stringify(response) + '\n');
381
+ };
382
+
383
+ // Capture output helper
384
+ const captureExec = (cmd) => {
385
+ try {
386
+ return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
387
+ } catch (e) {
388
+ return e.message;
389
+ }
390
+ };
391
+
392
+ // Tool handlers
393
+ const handleTool = async (name, args = {}) => {
394
+ switch (name) {
395
+ case 'deploy': {
396
+ if (!config.server || !config.remotePath) {
397
+ return { content: [{ type: 'text', text: 'Error: server and remotePath must be configured. Run "shsu init" first.' }] };
398
+ }
399
+ const funcName = args.name;
400
+ const noRestart = args.noRestart || false;
401
+ let output = '';
402
+
403
+ if (!funcName) {
404
+ output = captureExec(`rsync -avz --delete --exclude='*.test.ts' --exclude='*.spec.ts' "${config.localPath}/" "${config.server}:${config.remotePath}/"`);
405
+ } else {
406
+ const funcPath = join(config.localPath, funcName);
407
+ if (!existsSync(funcPath)) {
408
+ return { content: [{ type: 'text', text: `Error: Function not found: ${funcPath}` }] };
409
+ }
410
+ output = captureExec(`rsync -avz "${funcPath}/" "${config.server}:${config.remotePath}/${funcName}/"`);
411
+ }
412
+
413
+ if (!noRestart) {
414
+ output += '\n' + captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
415
+ }
416
+
417
+ return { content: [{ type: 'text', text: `Deployed${funcName ? ` ${funcName}` : ' all functions'}${noRestart ? ' (no restart)' : ''}\n\n${output}` }] };
418
+ }
419
+
420
+ case 'list': {
421
+ if (!config.server || !config.remotePath) {
422
+ return { content: [{ type: 'text', text: 'Error: server and remotePath must be configured.' }] };
423
+ }
424
+ const remote = captureExec(`ssh ${config.server} "ls -1 ${config.remotePath} 2>/dev/null"`) || '(none)';
425
+ let local = '(none)';
426
+ if (existsSync(config.localPath)) {
427
+ const dirs = readdirSync(config.localPath, { withFileTypes: true })
428
+ .filter((d) => d.isDirectory())
429
+ .map((d) => d.name);
430
+ local = dirs.length ? dirs.join('\n') : '(none)';
431
+ }
432
+ return { content: [{ type: 'text', text: `Remote functions:\n${remote}\n\nLocal functions:\n${local}` }] };
433
+ }
434
+
435
+ case 'invoke': {
436
+ if (!config.url) {
437
+ return { content: [{ type: 'text', text: 'Error: url must be configured for invoke.' }] };
438
+ }
439
+ if (!args.name) {
440
+ return { content: [{ type: 'text', text: 'Error: function name is required.' }] };
441
+ }
442
+ const data = args.data || '{}';
443
+ const output = captureExec(`curl -s -X POST "${config.url}/functions/v1/${args.name}" -H "Content-Type: application/json" -d '${data}'`);
444
+ return { content: [{ type: 'text', text: output }] };
445
+ }
446
+
447
+ case 'restart': {
448
+ if (!config.server) {
449
+ return { content: [{ type: 'text', text: 'Error: server must be configured.' }] };
450
+ }
451
+ const output = captureExec(`ssh ${config.server} "docker restart \\$(docker ps -q --filter 'name=edge')"`);
452
+ return { content: [{ type: 'text', text: `Restarted edge-runtime\n\n${output}` }] };
453
+ }
454
+
455
+ case 'new': {
456
+ if (!args.name) {
457
+ return { content: [{ type: 'text', text: 'Error: function name is required.' }] };
458
+ }
459
+ const funcPath = join(config.localPath, args.name);
460
+ if (existsSync(funcPath)) {
461
+ return { content: [{ type: 'text', text: `Error: Function already exists: ${args.name}` }] };
462
+ }
463
+ mkdirSync(funcPath, { recursive: true });
464
+ writeFileSync(
465
+ join(funcPath, 'index.ts'),
466
+ `Deno.serve(async (req) => {
467
+ try {
468
+ const { name } = await req.json()
469
+
470
+ return new Response(
471
+ JSON.stringify({ message: \`Hello \${name}!\` }),
472
+ { headers: { "Content-Type": "application/json" } }
473
+ )
474
+ } catch (error) {
475
+ return new Response(
476
+ JSON.stringify({ error: error.message }),
477
+ { status: 400, headers: { "Content-Type": "application/json" } }
478
+ )
479
+ }
480
+ })
481
+ `
482
+ );
483
+ return { content: [{ type: 'text', text: `Created ${funcPath}/index.ts` }] };
484
+ }
485
+
486
+ case 'config': {
487
+ return {
488
+ content: [{
489
+ type: 'text',
490
+ text: `Current configuration:\n server: ${config.server || '(not set)'}\n remotePath: ${config.remotePath || '(not set)'}\n url: ${config.url || '(not set)'}\n localPath: ${config.localPath}`,
491
+ }],
492
+ };
493
+ }
494
+
495
+ default:
496
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
497
+ }
498
+ };
499
+
500
+ // Process incoming messages
501
+ const rl = createInterface({ input: process.stdin });
502
+
503
+ for await (const line of rl) {
504
+ let msg;
505
+ try {
506
+ msg = JSON.parse(line);
507
+ } catch {
508
+ continue;
509
+ }
510
+
511
+ const { id, method, params } = msg;
512
+
513
+ switch (method) {
514
+ case 'initialize':
515
+ respond(id, {
516
+ protocolVersion: '2024-11-05',
517
+ capabilities: { tools: {} },
518
+ serverInfo,
519
+ });
520
+ break;
521
+
522
+ case 'notifications/initialized':
523
+ // No response needed for notifications
524
+ break;
525
+
526
+ case 'tools/list':
527
+ respond(id, { tools });
528
+ break;
529
+
530
+ case 'tools/call':
531
+ try {
532
+ const result = await handleTool(params.name, params.arguments || {});
533
+ respond(id, result);
534
+ } catch (e) {
535
+ respond(id, { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true });
536
+ }
537
+ break;
538
+
539
+ default:
540
+ if (id !== undefined) {
541
+ respondError(id, -32601, `Method not found: ${method}`);
542
+ }
543
+ }
544
+ }
545
+ }
546
+
311
547
  function cmdHelp() {
312
548
  console.log(`
313
549
  ${c.blue('shsu')} - Self-Hosted Supabase Utilities
@@ -336,6 +572,8 @@ ${c.yellow('Commands:')}
336
572
 
337
573
  env Show current configuration
338
574
 
575
+ mcp Start MCP server (for AI assistants)
576
+
339
577
  ${c.yellow('Examples:')}
340
578
  shsu init
341
579
  shsu deploy
@@ -386,6 +624,9 @@ async function main() {
386
624
  case 'init':
387
625
  await cmdInit();
388
626
  break;
627
+ case 'mcp':
628
+ await cmdMcp();
629
+ break;
389
630
  case 'env':
390
631
  case 'config':
391
632
  cmdEnv();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shsu",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
4
4
  "description": "CLI for deploying and managing Supabase Edge Functions on self-hosted Supabase (Coolify, Docker Compose). Sync functions via rsync, stream logs, and invoke endpoints.",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -31,4 +31,4 @@
31
31
  "url": "https://github.com/YUZU-Hub/shsu/issues"
32
32
  },
33
33
  "homepage": "https://github.com/YUZU-Hub/shsu#readme"
34
- }
34
+ }
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node bin/shsu.mjs:*)"
5
- ]
6
- }
7
- }