@mgsoftwarebv/mg-dashboard-mcp 2.2.4 → 2.3.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/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { ListToolsRequestSchema, CallToolRequestSchema, isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
6
+ import { createServer } from 'http';
7
+ import { randomUUID, createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
5
8
  import { createClient } from '@supabase/supabase-js';
6
- import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
7
9
  import { Client } from 'ssh2';
8
10
 
9
11
  // src/trigger-tools.ts
@@ -373,6 +375,835 @@ ${rawJson.substring(0, 500)}`
373
375
  }
374
376
  }
375
377
 
378
+ // src/script-tools.ts
379
+ var SCRIPTS = [
380
+ {
381
+ name: "commit-feedback",
382
+ filename: "commit-feedback.ps1",
383
+ description: "PowerShell script that provides colored terminal feedback for the Ctrl+Shift+K commit & push keybinding. Polls for a new commit and shows branch, message, and file stats when done.",
384
+ content: `param()
385
+
386
+ $initial = git log --oneline -1 2>$null
387
+
388
+ Write-Host "\`n==========================================" -ForegroundColor Cyan
389
+ Write-Host " Starting Commit & Push..." -ForegroundColor Cyan
390
+ Write-Host "==========================================" -ForegroundColor Cyan
391
+ Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
392
+ Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
393
+ Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
394
+ Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
395
+ Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
396
+ Write-Host ""
397
+ Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
398
+
399
+ $timeout = 60
400
+ $elapsed = 0
401
+ while ($elapsed -lt $timeout) {
402
+ $current = git log --oneline -1 2>$null
403
+ if ($current -ne $initial) { break }
404
+ Start-Sleep -Seconds 1
405
+ $elapsed++
406
+ Write-Host "." -ForegroundColor DarkGray -NoNewline
407
+ }
408
+ Write-Host ""
409
+
410
+ if ($current -eq $initial) {
411
+ Write-Host ""
412
+ Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
413
+ Write-Host ""
414
+ exit
415
+ }
416
+
417
+ Start-Sleep -Seconds 2
418
+
419
+ $branch = git rev-parse --abbrev-ref HEAD 2>$null
420
+ $commit = git log --format='%h %s' -1 2>$null
421
+
422
+ Write-Host ""
423
+ Write-Host "==========================================" -ForegroundColor Cyan
424
+ Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
425
+ Write-Host "==========================================" -ForegroundColor Cyan
426
+ Write-Host " Branch: $branch" -ForegroundColor Yellow
427
+ Write-Host " Commit: $commit" -ForegroundColor White
428
+ Write-Host ""
429
+ git --no-pager diff HEAD~1 --stat --color=always 2>$null
430
+ Write-Host ""
431
+ Write-Host "==========================================" -ForegroundColor Cyan
432
+ Write-Host ""
433
+ `
434
+ },
435
+ {
436
+ name: "create-pr",
437
+ filename: "create-pr.cmd",
438
+ description: "Windows batch script that auto-detects the target branch based on your release pipeline (v*-changes branches), merges the target into your branch, pushes, and creates a PR via gh CLI. Bound to Ctrl+Shift+J.",
439
+ content: `@echo off
440
+ setlocal enabledelayedexpansion
441
+ title MG Dashboard - Create PR
442
+
443
+ REM --- ANSI color setup ---
444
+ for /F %%a in ('echo prompt $E ^| cmd') do set "ESC=%%a"
445
+ set "GREEN=%ESC%[32m"
446
+ set "RED=%ESC%[31m"
447
+ set "YELLOW=%ESC%[33m"
448
+ set "CYAN=%ESC%[36m"
449
+ set "DIM=%ESC%[90m"
450
+ set "BOLD=%ESC%[1m"
451
+ set "RESET=%ESC%[0m"
452
+
453
+ echo.
454
+ echo %CYAN%%BOLD%==========================================%RESET%
455
+ echo %CYAN%%BOLD% MG Dashboard - Create Pull Request%RESET%
456
+ echo %CYAN%%BOLD%==========================================%RESET%
457
+ echo.
458
+
459
+ REM --- Merge origin/main first ---
460
+ echo %CYAN%[0/4]%RESET% Merging origin/main into current branch...
461
+ git fetch --prune >nul 2>&1
462
+ git merge origin/main --no-edit >nul 2>&1
463
+ if errorlevel 1 (
464
+ git merge --abort >nul 2>&1
465
+ echo %RED%X Merge conflict with origin/main. Resolve manually.%RESET%
466
+ echo.
467
+ exit /b 1
468
+ )
469
+ echo %GREEN% OK%RESET%
470
+ echo.
471
+
472
+ REM --- Prerequisite: gh CLI ---
473
+ where gh >nul 2>&1
474
+ if errorlevel 1 (
475
+ echo %RED%X GitHub CLI ^(gh^) is not installed.%RESET%
476
+ echo %DIM% Install it from: https://cli.github.com/%RESET%
477
+ echo.
478
+ exit /b 1
479
+ )
480
+
481
+ REM --- Prerequisite: git repo ---
482
+ git rev-parse --is-inside-work-tree >nul 2>&1
483
+ if errorlevel 1 (
484
+ echo %RED%X Not inside a git repository.%RESET%
485
+ echo.
486
+ exit /b 1
487
+ )
488
+
489
+ REM --- Detect repo from git remote ---
490
+ set "REPO="
491
+ for /f "tokens=*" %%u in ('git remote get-url origin 2^>nul') do set "REMOTE_URL=%%u"
492
+ if defined REMOTE_URL (
493
+ echo !REMOTE_URL! | findstr /r "git@github.com:" >nul 2>&1
494
+ if not errorlevel 1 (
495
+ set "REPO=!REMOTE_URL:git@github.com:=!"
496
+ set "REPO=!REPO:.git=!"
497
+ )
498
+ echo !REMOTE_URL! | findstr /r "https://github.com/" >nul 2>&1
499
+ if not errorlevel 1 (
500
+ set "REPO=!REMOTE_URL:https://github.com/=!"
501
+ set "REPO=!REPO:.git=!"
502
+ )
503
+ )
504
+ if not defined REPO (
505
+ echo %RED%X Could not detect GitHub repo from git remote.%RESET%
506
+ echo %DIM% Make sure origin points to a GitHub repository.%RESET%
507
+ echo.
508
+ exit /b 1
509
+ )
510
+
511
+ REM --- Get current branch ---
512
+ for /f "tokens=*" %%b in ('git rev-parse --abbrev-ref HEAD') do set "CURRENT_BRANCH=%%b"
513
+
514
+ echo %DIM% Repository: !REPO!%RESET%
515
+ echo %DIM% Branch: %CURRENT_BRANCH%%RESET%
516
+ echo.
517
+
518
+ REM --- Guard: don't PR from main or a bare version branch ---
519
+ if "%CURRENT_BRANCH%"=="main" (
520
+ echo %RED%X You are on main. Switch to a feature or changes branch first.%RESET%
521
+ echo.
522
+ exit /b 1
523
+ )
524
+ echo %CURRENT_BRANCH% | findstr /r "^v[0-9]*\\.[0-9]*\\.[0-9]*$" >nul 2>&1
525
+ if not errorlevel 1 (
526
+ echo %RED%X You are on a version branch ^(%CURRENT_BRANCH%^). Switch to a feature or changes branch first.%RESET%
527
+ echo.
528
+ exit /b 1
529
+ )
530
+
531
+ REM --- Fetch latest remote refs ---
532
+ echo %CYAN%[1/4]%RESET% Fetching remote branches...
533
+ git fetch --prune >nul 2>&1
534
+
535
+ REM --- Determine target branch ---
536
+ set "TARGET="
537
+
538
+ REM Check if current branch is a -changes branch (last 8 chars)
539
+ if "!CURRENT_BRANCH:~-8!"=="-changes" (
540
+ set "TARGET=!CURRENT_BRANCH:~0,-8!"
541
+ echo %GREEN% Detected -changes branch. Target: %BOLD%!TARGET!%RESET%
542
+ goto :do_merge
543
+ )
544
+
545
+ REM Look for v*-changes branches on remote (highest version first)
546
+ set "TARGET="
547
+ for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*-changes" 2^>nul') do (
548
+ if not defined TARGET (
549
+ set "RAW=%%r"
550
+ set "BRANCH=!RAW: origin/=!"
551
+ set "BRANCH=!BRANCH:origin/=!"
552
+ set "TARGET=!BRANCH!"
553
+ )
554
+ )
555
+
556
+ if defined TARGET (
557
+ echo %GREEN% Release pipeline detected. Target: %BOLD%!TARGET!%RESET%
558
+ ) else (
559
+ REM No -changes branch; try highest version branch
560
+ set "TARGET="
561
+ for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*" 2^>nul') do (
562
+ if not defined TARGET (
563
+ set "RAW=%%r"
564
+ set "BRANCH=!RAW: origin/=!"
565
+ set "BRANCH=!BRANCH:origin/=!"
566
+ set "TAIL=!BRANCH:~-8!"
567
+ if not "!TAIL!"=="-changes" set "TARGET=!BRANCH!"
568
+ )
569
+ )
570
+ if defined TARGET (
571
+ echo %GREEN% Version branch detected. Target: %BOLD%!TARGET!%RESET%
572
+ ) else (
573
+ set "TARGET=main"
574
+ echo %YELLOW% No release pipeline or version branch found. Target: %BOLD%main%RESET%
575
+ )
576
+ )
577
+
578
+ :do_merge
579
+ echo.
580
+
581
+ REM --- Merge target into current branch ---
582
+ echo %CYAN%[2/4]%RESET% Merging %BOLD%!TARGET!%RESET% into %BOLD%%CURRENT_BRANCH%%RESET%...
583
+ git merge origin/!TARGET! --no-edit
584
+ if errorlevel 1 (
585
+ echo.
586
+ echo %RED%X Merge conflict detected.%RESET%
587
+ git merge --abort >nul 2>&1
588
+ echo %DIM% Resolve conflicts manually, then retry.%RESET%
589
+ echo.
590
+ exit /b 1
591
+ )
592
+ echo %GREEN% OK%RESET%
593
+ echo.
594
+
595
+ REM --- Push current branch to origin ---
596
+ echo %CYAN%[3/4]%RESET% Pushing %CURRENT_BRANCH% to origin...
597
+ git push -u origin HEAD
598
+ if errorlevel 1 (
599
+ echo.
600
+ echo %RED%X Failed to push branch to origin.%RESET%
601
+ echo.
602
+ exit /b 1
603
+ )
604
+ echo %GREEN% OK%RESET%
605
+ echo.
606
+
607
+ REM --- Guard: don't PR into the same branch ---
608
+ if "!TARGET!"=="!CURRENT_BRANCH!" (
609
+ echo %RED%X Target branch is the same as current branch ^(!TARGET!^). Cannot create PR.%RESET%
610
+ echo.
611
+ exit /b 1
612
+ )
613
+
614
+ echo %CYAN%[4/4]%RESET% Creating PR: %BOLD%%CURRENT_BRANCH%%RESET% %DIM%->%RESET% %BOLD%%TARGET%%RESET%
615
+ echo.
616
+
617
+ REM --- Create the PR ---
618
+ gh pr create --repo "!REPO!" --base "%TARGET%" --fill
619
+ if errorlevel 1 (
620
+ echo.
621
+ echo %RED%X Failed to create pull request.%RESET%
622
+ echo %DIM% Check the error above. A PR may already exist for this branch.%RESET%
623
+ echo.
624
+ exit /b 1
625
+ )
626
+
627
+ echo.
628
+ echo %GREEN%%BOLD% PR created successfully!%RESET%
629
+ echo.
630
+ `
631
+ },
632
+ {
633
+ name: "commit-and-pr",
634
+ filename: "commit-and-pr.ps1",
635
+ description: "PowerShell script that combines commit & push with PR creation. Checks for pending changes first: if changes exist, waits for the commit (triggered by Cursor keybinding), shows a summary, then runs create-pr.cmd. If no changes, skips straight to PR. Requires create-pr.cmd in the same directory. Bound to Ctrl+Shift+M.",
636
+ content: `param()
637
+
638
+ $hasChanges = (git status --porcelain 2>$null)
639
+
640
+ if ($hasChanges) {
641
+ $initial = git log --oneline -1 2>$null
642
+
643
+ Write-Host "\`n==========================================" -ForegroundColor Cyan
644
+ Write-Host " Starting Commit, Push & PR..." -ForegroundColor Cyan
645
+ Write-Host "==========================================" -ForegroundColor Cyan
646
+ Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
647
+ Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
648
+ Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
649
+ Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
650
+ Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
651
+ Write-Host ""
652
+ Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
653
+
654
+ $timeout = 60
655
+ $elapsed = 0
656
+ while ($elapsed -lt $timeout) {
657
+ $current = git log --oneline -1 2>$null
658
+ if ($current -ne $initial) { break }
659
+ Start-Sleep -Seconds 1
660
+ $elapsed++
661
+ Write-Host "." -ForegroundColor DarkGray -NoNewline
662
+ }
663
+ Write-Host ""
664
+
665
+ if ($current -eq $initial) {
666
+ Write-Host ""
667
+ Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
668
+ Write-Host " Skipping PR creation." -ForegroundColor Yellow
669
+ Write-Host ""
670
+ exit
671
+ }
672
+
673
+ Start-Sleep -Seconds 2
674
+
675
+ $branch = git rev-parse --abbrev-ref HEAD 2>$null
676
+ $commit = git log --format='%h %s' -1 2>$null
677
+
678
+ Write-Host ""
679
+ Write-Host "==========================================" -ForegroundColor Cyan
680
+ Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
681
+ Write-Host "==========================================" -ForegroundColor Cyan
682
+ Write-Host " Branch: $branch" -ForegroundColor Yellow
683
+ Write-Host " Commit: $commit" -ForegroundColor White
684
+ Write-Host ""
685
+ git --no-pager diff HEAD~1 --stat --color=always 2>$null
686
+ Write-Host ""
687
+ Write-Host "==========================================" -ForegroundColor Cyan
688
+ Write-Host ""
689
+ } else {
690
+ Write-Host "\`n==========================================" -ForegroundColor Cyan
691
+ Write-Host " No changes to commit" -ForegroundColor DarkGray
692
+ Write-Host " Proceeding to PR creation..." -ForegroundColor Cyan
693
+ Write-Host "==========================================" -ForegroundColor Cyan
694
+ Write-Host ""
695
+ }
696
+
697
+ & .\\create-pr.cmd
698
+ `
699
+ }
700
+ ];
701
+ var SCRIPT_TOOLS = [
702
+ {
703
+ name: "scripts-list",
704
+ description: "List available MG Dashboard workflow scripts that can be downloaded into your project. Returns script names, filenames, and descriptions.",
705
+ inputSchema: { type: "object", properties: {}, required: [] }
706
+ },
707
+ {
708
+ name: "script-get",
709
+ description: "Get the content of an MG Dashboard workflow script by name. Save the returned content to the filename indicated in the response. Place scripts in the repository root.",
710
+ inputSchema: {
711
+ type: "object",
712
+ properties: {
713
+ name: {
714
+ type: "string",
715
+ description: `Script name. Available: ${SCRIPTS.map((s) => s.name).join(", ")}`
716
+ }
717
+ },
718
+ required: ["name"]
719
+ }
720
+ }
721
+ ];
722
+ var SCRIPT_TOOL_NAMES = new Set(SCRIPT_TOOLS.map((t) => t.name));
723
+ function handleScriptTool(toolName, args2) {
724
+ switch (toolName) {
725
+ case "scripts-list": {
726
+ const list = SCRIPTS.map((s) => ({
727
+ name: s.name,
728
+ filename: s.filename,
729
+ description: s.description
730
+ }));
731
+ return {
732
+ content: [
733
+ {
734
+ type: "text",
735
+ text: JSON.stringify(list, null, 2)
736
+ }
737
+ ]
738
+ };
739
+ }
740
+ case "script-get": {
741
+ const scriptName = String(args2.name);
742
+ const script = SCRIPTS.find((s) => s.name === scriptName);
743
+ if (!script) {
744
+ const available = SCRIPTS.map((s) => s.name).join(", ");
745
+ return {
746
+ content: [
747
+ {
748
+ type: "text",
749
+ text: `Unknown script: "${scriptName}". Available scripts: ${available}`
750
+ }
751
+ ]
752
+ };
753
+ }
754
+ return {
755
+ content: [
756
+ {
757
+ type: "text",
758
+ text: [
759
+ `Filename: ${script.filename}`,
760
+ `Description: ${script.description}`,
761
+ `Place this file in the repository root.`,
762
+ "",
763
+ "--- CONTENT START ---",
764
+ script.content,
765
+ "--- CONTENT END ---"
766
+ ].join("\n")
767
+ }
768
+ ]
769
+ };
770
+ }
771
+ default:
772
+ return { content: [{ type: "text", text: `Unknown script tool: ${toolName}` }] };
773
+ }
774
+ }
775
+
776
+ // src/agent-tools.ts
777
+ var VALID_FINDING_TYPES = /* @__PURE__ */ new Set([
778
+ "missing_docs",
779
+ "inaccurate",
780
+ "incomplete",
781
+ "outdated",
782
+ "improvement",
783
+ "n_plus_1_query",
784
+ "bundle_size",
785
+ "unnecessary_rerender",
786
+ "slow_endpoint",
787
+ "memory_leak",
788
+ "missing_memoization",
789
+ "large_dependency"
790
+ ]);
791
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "critical"]);
792
+ var VALID_SCOPES = /* @__PURE__ */ new Set([
793
+ "architecture",
794
+ "module",
795
+ "component",
796
+ "api",
797
+ "package"
798
+ ]);
799
+ var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
800
+ "pending",
801
+ "approved",
802
+ "rejected",
803
+ "outdated"
804
+ ]);
805
+ var AGENT_TOOLS = [
806
+ {
807
+ name: "agent-report-coverage",
808
+ description: "Report documentation coverage data for a repository. Upserts records into doc_coverage. Call once per logical unit (package, module, theme, plugin directory). Provide all entries in a single call for efficiency.",
809
+ inputSchema: {
810
+ type: "object",
811
+ properties: {
812
+ repo_slug: {
813
+ type: "string",
814
+ description: 'Repository slug (e.g. "mg-dashboard", "bna-wordpress")'
815
+ },
816
+ refront_project_id: {
817
+ type: "string",
818
+ description: "Refront project UUID linked to this repository"
819
+ },
820
+ entries: {
821
+ type: "array",
822
+ description: "Array of coverage entries, one per logical unit",
823
+ items: {
824
+ type: "object",
825
+ properties: {
826
+ path: { type: "string", description: 'Logical unit path (e.g. "packages/ui", "wp-content/themes/bna")' },
827
+ total_functions: { type: "number", description: "Total public functions/methods" },
828
+ documented_functions: { type: "number", description: "Functions with doc comments" },
829
+ total_types: { type: "number", description: "Total classes/interfaces/types" },
830
+ documented_types: { type: "number", description: "Types with doc comments" },
831
+ total_endpoints: { type: "number", description: "Total API/REST/hook endpoints" },
832
+ documented_endpoints: { type: "number", description: "Endpoints with documentation" }
833
+ },
834
+ required: ["path", "total_functions", "documented_functions"]
835
+ }
836
+ }
837
+ },
838
+ required: ["repo_slug", "refront_project_id", "entries"]
839
+ }
840
+ },
841
+ {
842
+ name: "agent-report-finding",
843
+ description: "Report documentation or performance findings for a repository. Inserts records into doc_suggestion. Auto-creates a parent documentation record if one does not exist yet. Batch multiple findings in one call.",
844
+ inputSchema: {
845
+ type: "object",
846
+ properties: {
847
+ repo_slug: {
848
+ type: "string",
849
+ description: "Repository slug"
850
+ },
851
+ refront_project_id: {
852
+ type: "string",
853
+ description: "Refront project UUID linked to this repository"
854
+ },
855
+ category: {
856
+ type: "string",
857
+ enum: ["scan_findings", "perf_audit"],
858
+ description: 'Finding category: "scan_findings" for doc issues, "perf_audit" for performance issues'
859
+ },
860
+ findings: {
861
+ type: "array",
862
+ description: "Array of findings to report",
863
+ items: {
864
+ type: "object",
865
+ properties: {
866
+ type: {
867
+ type: "string",
868
+ enum: [...VALID_FINDING_TYPES],
869
+ description: "Finding type"
870
+ },
871
+ severity: {
872
+ type: "string",
873
+ enum: ["info", "warning", "critical"],
874
+ description: "Severity level"
875
+ },
876
+ description: {
877
+ type: "string",
878
+ description: "Clear description of the issue (max 2000 chars)"
879
+ },
880
+ file_path: {
881
+ type: "string",
882
+ description: "Relative file path where the issue was found"
883
+ },
884
+ suggested_fix: {
885
+ type: "string",
886
+ description: "Suggested fix with code example (max 5000 chars)"
887
+ }
888
+ },
889
+ required: ["type", "severity", "description"]
890
+ }
891
+ }
892
+ },
893
+ required: ["repo_slug", "refront_project_id", "category", "findings"]
894
+ }
895
+ },
896
+ {
897
+ name: "agent-save-documentation",
898
+ description: "Save or update a documentation record for a repository. Upserts by repo_slug + scope + path combination.",
899
+ inputSchema: {
900
+ type: "object",
901
+ properties: {
902
+ repo_slug: { type: "string", description: "Repository slug" },
903
+ refront_project_id: { type: "string", description: "Refront project UUID" },
904
+ scope: {
905
+ type: "string",
906
+ enum: [...VALID_SCOPES],
907
+ description: "Documentation scope"
908
+ },
909
+ path: {
910
+ type: "string",
911
+ description: 'Path within the repo (e.g. "packages/ui", "__perf_audit__")'
912
+ },
913
+ title: { type: "string", description: "Document title" },
914
+ content: { type: "string", description: "Documentation content (markdown)" },
915
+ review_status: {
916
+ type: "string",
917
+ enum: [...VALID_REVIEW_STATUSES],
918
+ description: 'Review status (default: "pending")'
919
+ }
920
+ },
921
+ required: ["repo_slug", "refront_project_id", "scope", "path", "title", "content"]
922
+ }
923
+ },
924
+ {
925
+ name: "agent-list-findings",
926
+ description: "List existing findings (doc_suggestion records) for a repository. Use this to check what has already been reported before submitting new findings.",
927
+ inputSchema: {
928
+ type: "object",
929
+ properties: {
930
+ repo_slug: { type: "string", description: "Repository slug" },
931
+ type: {
932
+ type: "string",
933
+ enum: [...VALID_FINDING_TYPES],
934
+ description: "Filter by finding type"
935
+ },
936
+ severity: {
937
+ type: "string",
938
+ enum: ["info", "warning", "critical"],
939
+ description: "Filter by severity"
940
+ },
941
+ status: {
942
+ type: "string",
943
+ enum: ["open", "accepted", "rejected", "fixed"],
944
+ description: "Filter by status (default: all)"
945
+ },
946
+ limit: {
947
+ type: "number",
948
+ description: "Max results to return (default: 50, max: 200)"
949
+ }
950
+ },
951
+ required: ["repo_slug"]
952
+ }
953
+ },
954
+ {
955
+ name: "agent-get-documentation",
956
+ description: "Retrieve existing documentation records for a repository. Use this to read current docs before generating or reviewing.",
957
+ inputSchema: {
958
+ type: "object",
959
+ properties: {
960
+ repo_slug: { type: "string", description: "Repository slug" },
961
+ scope: {
962
+ type: "string",
963
+ enum: [...VALID_SCOPES],
964
+ description: "Filter by scope"
965
+ },
966
+ path: { type: "string", description: "Filter by exact path" },
967
+ limit: {
968
+ type: "number",
969
+ description: "Max results to return (default: 20, max: 100)"
970
+ }
971
+ },
972
+ required: ["repo_slug"]
973
+ }
974
+ }
975
+ ];
976
+ var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
977
+ var AGENT_TOOL_MODULE_MAP = {
978
+ "agent-report-coverage": "agent_reporting",
979
+ "agent-report-finding": "agent_reporting",
980
+ "agent-save-documentation": "agent_reporting",
981
+ "agent-list-findings": "agent_reporting",
982
+ "agent-get-documentation": "agent_reporting"
983
+ };
984
+ function clamp(val, min, max) {
985
+ return Math.max(min, Math.min(max, val));
986
+ }
987
+ function sanitizeString(val, maxLen) {
988
+ return String(val ?? "").slice(0, maxLen);
989
+ }
990
+ async function getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category) {
991
+ const path = category === "perf_audit" ? "__perf_audit__" : "__scan_findings__";
992
+ const title = category === "perf_audit" ? `Performance Audit: ${repoSlug}` : `Scan Findings: ${repoSlug}`;
993
+ const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("path", path).maybeSingle();
994
+ if (existing) return existing.id;
995
+ const { data: inserted, error } = await supabase2.from("project_documentation").insert({
996
+ refront_project_id: refrontProjectId,
997
+ repo_slug: repoSlug,
998
+ scope: "architecture",
999
+ path,
1000
+ title,
1001
+ content: `Auto-generated parent record for ${category.replace("_", " ")} suggestions.`,
1002
+ generated_by: `${category}-agent-v1`,
1003
+ review_status: "pending"
1004
+ }).select("id").single();
1005
+ if (error || !inserted) {
1006
+ throw new Error(`Failed to create parent doc record: ${error?.message}`);
1007
+ }
1008
+ return inserted.id;
1009
+ }
1010
+ async function handleAgentTool(name, args2, deps) {
1011
+ const { supabase: supabase2 } = deps;
1012
+ switch (name) {
1013
+ // -----------------------------------------------------------------
1014
+ case "agent-report-coverage": {
1015
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
1016
+ const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1017
+ const entries = Array.isArray(args2.entries) ? args2.entries : [];
1018
+ if (!repoSlug) throw new Error("repo_slug is required");
1019
+ if (!refrontProjectId) throw new Error("refront_project_id is required");
1020
+ if (entries.length === 0) throw new Error("entries array must not be empty");
1021
+ const wsId = deps.workspaceId;
1022
+ const scanCommit = wsId ? `agent-scan-${wsId.slice(0, 8)}` : `agent-scan-${Date.now().toString(36)}`;
1023
+ let upserted = 0;
1024
+ let errors = 0;
1025
+ for (const entry of entries) {
1026
+ const { error } = await supabase2.from("doc_coverage").upsert(
1027
+ {
1028
+ refront_project_id: refrontProjectId,
1029
+ repo_slug: repoSlug,
1030
+ path: sanitizeString(entry.path, 500),
1031
+ total_functions: clamp(Number(entry.total_functions) || 0, 0, 99999),
1032
+ documented_functions: clamp(Number(entry.documented_functions) || 0, 0, 99999),
1033
+ total_types: clamp(Number(entry.total_types) || 0, 0, 99999),
1034
+ documented_types: clamp(Number(entry.documented_types) || 0, 0, 99999),
1035
+ total_endpoints: clamp(Number(entry.total_endpoints) || 0, 0, 99999),
1036
+ documented_endpoints: clamp(Number(entry.documented_endpoints) || 0, 0, 99999),
1037
+ scan_commit: scanCommit,
1038
+ scanned_at: (/* @__PURE__ */ new Date()).toISOString()
1039
+ },
1040
+ { onConflict: "repo_slug,path" }
1041
+ );
1042
+ if (error) errors++;
1043
+ else upserted++;
1044
+ }
1045
+ return {
1046
+ content: [{
1047
+ type: "text",
1048
+ text: `Coverage reported: ${upserted} entries upserted${errors > 0 ? `, ${errors} errors` : ""}`
1049
+ }]
1050
+ };
1051
+ }
1052
+ // -----------------------------------------------------------------
1053
+ case "agent-report-finding": {
1054
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
1055
+ const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1056
+ const category = args2.category;
1057
+ const findings = Array.isArray(args2.findings) ? args2.findings : [];
1058
+ if (!repoSlug) throw new Error("repo_slug is required");
1059
+ if (!refrontProjectId) throw new Error("refront_project_id is required");
1060
+ if (!category || !["scan_findings", "perf_audit"].includes(category)) {
1061
+ throw new Error('category must be "scan_findings" or "perf_audit"');
1062
+ }
1063
+ if (findings.length === 0) throw new Error("findings array must not be empty");
1064
+ const parentDocId = await getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category);
1065
+ let inserted = 0;
1066
+ let errors = 0;
1067
+ for (const f of findings) {
1068
+ const findingType = VALID_FINDING_TYPES.has(f.type) ? f.type : "improvement";
1069
+ const severity = VALID_SEVERITIES.has(f.severity) ? f.severity : "info";
1070
+ const description = sanitizeString(f.description, 2e3);
1071
+ if (!description) continue;
1072
+ const { error } = await supabase2.from("doc_suggestion").insert({
1073
+ documentation_id: parentDocId,
1074
+ type: findingType,
1075
+ severity,
1076
+ description,
1077
+ file_path: f.file_path ? sanitizeString(f.file_path, 500) : null,
1078
+ suggested_fix: f.suggested_fix ? sanitizeString(f.suggested_fix, 5e3) : null,
1079
+ status: "open"
1080
+ });
1081
+ if (error) errors++;
1082
+ else inserted++;
1083
+ }
1084
+ return {
1085
+ content: [{
1086
+ type: "text",
1087
+ text: `Findings reported: ${inserted} inserted under ${category}${errors > 0 ? `, ${errors} errors` : ""}`
1088
+ }]
1089
+ };
1090
+ }
1091
+ // -----------------------------------------------------------------
1092
+ case "agent-save-documentation": {
1093
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
1094
+ const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1095
+ const scope = VALID_SCOPES.has(args2.scope) ? args2.scope : "module";
1096
+ const path = sanitizeString(args2.path, 500);
1097
+ const title = sanitizeString(args2.title, 500);
1098
+ const content = sanitizeString(args2.content, 1e5);
1099
+ const reviewStatus = VALID_REVIEW_STATUSES.has(args2.review_status) ? args2.review_status : "pending";
1100
+ if (!repoSlug) throw new Error("repo_slug is required");
1101
+ if (!refrontProjectId) throw new Error("refront_project_id is required");
1102
+ if (!path) throw new Error("path is required");
1103
+ if (!title) throw new Error("title is required");
1104
+ if (!content) throw new Error("content is required");
1105
+ const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("scope", scope).eq("path", path).maybeSingle();
1106
+ if (existing) {
1107
+ const { error: error2 } = await supabase2.from("project_documentation").update({
1108
+ title,
1109
+ content,
1110
+ review_status: reviewStatus,
1111
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1112
+ }).eq("id", existing.id);
1113
+ if (error2) throw new Error(`Failed to update documentation: ${error2.message}`);
1114
+ return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path})` }] };
1115
+ }
1116
+ const generatedBy = deps.workspaceId ? `agent-${deps.workspaceId.slice(0, 8)}` : "agent-mcp";
1117
+ const { error } = await supabase2.from("project_documentation").insert({
1118
+ refront_project_id: refrontProjectId,
1119
+ repo_slug: repoSlug,
1120
+ scope,
1121
+ path,
1122
+ title,
1123
+ content,
1124
+ generated_by: generatedBy,
1125
+ review_status: reviewStatus
1126
+ });
1127
+ if (error) throw new Error(`Failed to save documentation: ${error.message}`);
1128
+ return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path})` }] };
1129
+ }
1130
+ // -----------------------------------------------------------------
1131
+ case "agent-list-findings": {
1132
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
1133
+ if (!repoSlug) throw new Error("repo_slug is required");
1134
+ const limit = clamp(Number(args2.limit) || 50, 1, 200);
1135
+ let docQuery = supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug);
1136
+ const docIds = await docQuery;
1137
+ if (!docIds.data || docIds.data.length === 0) {
1138
+ return { content: [{ type: "text", text: `No documentation records found for repo "${repoSlug}"` }] };
1139
+ }
1140
+ let query = supabase2.from("doc_suggestion").select("id, type, severity, description, file_path, status, created_at").in("documentation_id", docIds.data.map((d) => d.id)).order("created_at", { ascending: false }).limit(limit);
1141
+ if (args2.type && VALID_FINDING_TYPES.has(args2.type)) {
1142
+ query = query.eq("type", args2.type);
1143
+ }
1144
+ if (args2.severity && VALID_SEVERITIES.has(args2.severity)) {
1145
+ query = query.eq("severity", args2.severity);
1146
+ }
1147
+ if (args2.status) {
1148
+ query = query.eq("status", args2.status);
1149
+ }
1150
+ const { data: findings, error } = await query;
1151
+ if (error) throw new Error(`Failed to query findings: ${error.message}`);
1152
+ if (!findings || findings.length === 0) {
1153
+ return { content: [{ type: "text", text: `No findings found for repo "${repoSlug}"` }] };
1154
+ }
1155
+ const summary = findings.map(
1156
+ (f) => `[${f.severity}] ${f.type}: ${String(f.description).slice(0, 120)}${f.file_path ? ` (${f.file_path})` : ""} \u2014 ${f.status}`
1157
+ ).join("\n");
1158
+ return {
1159
+ content: [{
1160
+ type: "text",
1161
+ text: `${findings.length} findings for "${repoSlug}":
1162
+
1163
+ ${summary}`
1164
+ }]
1165
+ };
1166
+ }
1167
+ // -----------------------------------------------------------------
1168
+ case "agent-get-documentation": {
1169
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
1170
+ if (!repoSlug) throw new Error("repo_slug is required");
1171
+ const limit = clamp(Number(args2.limit) || 20, 1, 100);
1172
+ let query = supabase2.from("project_documentation").select("id, repo_slug, scope, path, title, content, review_status, generated_by, created_at, updated_at").eq("repo_slug", repoSlug).order("updated_at", { ascending: false }).limit(limit);
1173
+ if (args2.scope && VALID_SCOPES.has(args2.scope)) {
1174
+ query = query.eq("scope", args2.scope);
1175
+ }
1176
+ if (args2.path) {
1177
+ query = query.eq("path", sanitizeString(args2.path, 500));
1178
+ }
1179
+ const { data: docs, error } = await query;
1180
+ if (error) throw new Error(`Failed to query documentation: ${error.message}`);
1181
+ if (!docs || docs.length === 0) {
1182
+ return { content: [{ type: "text", text: `No documentation found for repo "${repoSlug}"` }] };
1183
+ }
1184
+ const output = docs.map((d) => {
1185
+ const contentPreview = String(d.content || "").slice(0, 500);
1186
+ return [
1187
+ `## ${d.title} (${d.scope}/${d.path})`,
1188
+ `Status: ${d.review_status} | By: ${d.generated_by || "unknown"}`,
1189
+ `Updated: ${d.updated_at || d.created_at}`,
1190
+ "",
1191
+ contentPreview + (String(d.content || "").length > 500 ? "\n...(truncated)" : ""),
1192
+ ""
1193
+ ].join("\n");
1194
+ }).join("\n---\n\n");
1195
+ return {
1196
+ content: [{ type: "text", text: `${docs.length} docs for "${repoSlug}":
1197
+
1198
+ ${output}` }]
1199
+ };
1200
+ }
1201
+ // -----------------------------------------------------------------
1202
+ default:
1203
+ return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
1204
+ }
1205
+ }
1206
+
376
1207
  // src/index.ts
377
1208
  var args = process.argv.slice(2);
378
1209
  function getArg(name) {
@@ -383,6 +1214,9 @@ var supabaseUrl = getArg("supabase-url") || process.env.SUPABASE_URL;
383
1214
  var supabaseKey = getArg("supabase-key") || process.env.SUPABASE_SERVICE_ROLE_KEY;
384
1215
  var encryptionKey = getArg("encryption-key") || process.env.ENCRYPTION_KEY;
385
1216
  var mijnhostApiKey = getArg("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
1217
+ var agentWorkspaceId = getArg("workspace-id") || process.env.AGENT_WORKSPACE_ID || null;
1218
+ var httpMode = args.includes("--http");
1219
+ var httpPort = Number(getArg("port")) || 3100;
386
1220
  if (!apiKey) {
387
1221
  console.error("API key is required. Use --api-key=dk_xxx or set MG_DASHBOARD_API_KEY");
388
1222
  process.exit(1);
@@ -439,16 +1273,14 @@ setInterval(() => {
439
1273
  }, 5 * 60 * 1e3).unref();
440
1274
  var MODULE_KEYS = [
441
1275
  "users",
442
- "emails",
443
- "logs",
444
1276
  "ssh_servers",
445
1277
  "supabase",
446
1278
  "wiki",
447
1279
  "ci_cd",
448
1280
  "source_control",
449
1281
  "domains",
450
- "google_search_console",
451
- "settings"
1282
+ "settings",
1283
+ "agent_reporting"
452
1284
  ];
453
1285
  var FULL_PERMISSIONS = {
454
1286
  modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
@@ -487,28 +1319,18 @@ function intersectServerAccess(keyServerIds, permissionServerIds) {
487
1319
  }
488
1320
  var TOOL_MODULE_MAP = {
489
1321
  "list-servers": "ssh_servers",
490
- "server-status": "ssh_servers",
491
1322
  "ssh-execute": "ssh_servers",
492
- "server-reboot": "ssh_servers",
493
- "server-restart-service": "ssh_servers",
494
1323
  "sftp-list": "ssh_servers",
495
1324
  "sftp-read": "ssh_servers",
496
1325
  "sftp-write": "ssh_servers",
497
1326
  "sftp-delete": "ssh_servers",
498
1327
  "docker-list": "ssh_servers",
499
- "docker-action": "ssh_servers",
500
1328
  "docker-logs": "ssh_servers",
501
1329
  "db-discover": "ssh_servers",
502
1330
  "db-tables": "ssh_servers",
503
1331
  "db-describe": "ssh_servers",
504
1332
  "db-query": "ssh_servers",
505
1333
  "cache-purge": "ssh_servers",
506
- "log-list": "ssh_servers",
507
- "log-read": "ssh_servers",
508
- "cron-list": "ssh_servers",
509
- "cron-add": "ssh_servers",
510
- "cron-remove": "ssh_servers",
511
- "cron-toggle": "ssh_servers",
512
1334
  "env-list": "ci_cd",
513
1335
  "env-get": "ci_cd",
514
1336
  "env-store": "ci_cd",
@@ -518,7 +1340,8 @@ var TOOL_MODULE_MAP = {
518
1340
  "dns-create": "domains",
519
1341
  "dns-update": "domains",
520
1342
  "dns-delete": "domains",
521
- ...TRIGGER_TOOL_MODULE_MAP
1343
+ ...TRIGGER_TOOL_MODULE_MAP,
1344
+ ...AGENT_TOOL_MODULE_MAP
522
1345
  };
523
1346
  var authContext = null;
524
1347
  async function validateApiKey(key) {
@@ -660,6 +1483,11 @@ function stageToVercelTargets(stageType, customEnvId) {
660
1483
  if (customEnvId) return { customEnvironmentIds: [customEnvId] };
661
1484
  return { target: ["preview"] };
662
1485
  }
1486
+ function getVercelEnvSyncTargetings(stageType, customEnvId) {
1487
+ const primary = stageToVercelTargets(stageType, customEnvId);
1488
+ if (stageType !== "dev") return [primary];
1489
+ return [primary, { target: ["development"] }];
1490
+ }
663
1491
  async function syncEnvVarsToVercel(token, projectId, envVars) {
664
1492
  if (envVars.length === 0) return { created: 0, error: null };
665
1493
  const res = await fetch(
@@ -735,18 +1563,24 @@ async function attemptVercelSync(appName, environment, knownStageId) {
735
1563
  syncResults.push(`${app.label}: empty config`);
736
1564
  continue;
737
1565
  }
738
- const targeting = stageToVercelTargets(stageType, app.vercelCustomEnvId);
739
- const envVars = keys.map((key) => ({
740
- key,
741
- value: pairs[key],
742
- type: "encrypted",
743
- ...targeting
744
- }));
745
- const { created, error } = await syncEnvVarsToVercel(token, app.vercelProjectId, envVars);
746
- if (error) {
747
- syncResults.push(`${app.label}: FAILED - ${error}`);
1566
+ const targetings = getVercelEnvSyncTargetings(stageType, app.vercelCustomEnvId);
1567
+ let createdTotal = 0;
1568
+ let lastErr = null;
1569
+ for (const targeting of targetings) {
1570
+ const envVars = keys.map((key) => ({
1571
+ key,
1572
+ value: pairs[key],
1573
+ type: "plain",
1574
+ ...targeting
1575
+ }));
1576
+ const { created, error } = await syncEnvVarsToVercel(token, app.vercelProjectId, envVars);
1577
+ if (error) lastErr = error;
1578
+ else createdTotal += created;
1579
+ }
1580
+ if (lastErr) {
1581
+ syncResults.push(`${app.label}: FAILED - ${lastErr}`);
748
1582
  } else {
749
- syncResults.push(`${app.label}: ${created} vars synced`);
1583
+ syncResults.push(`${app.label}: ${createdTotal} var upsert(s) synced`);
750
1584
  }
751
1585
  }
752
1586
  return `Vercel sync: ${syncResults.join("; ")}`;
@@ -1215,16 +2049,6 @@ function assertSafeCommand(command) {
1215
2049
  }
1216
2050
  }
1217
2051
  }
1218
- var ALLOWED_LOG_PREFIXES = ["/var/log/", "/usr/local/lsws/logs/", "/var/www/"];
1219
- function assertAllowedLogPath(filePath) {
1220
- const safe = sanitizePath(filePath);
1221
- if (!safe.endsWith(".log")) {
1222
- throw new Error("Only .log files can be read with log-read");
1223
- }
1224
- if (!ALLOWED_LOG_PREFIXES.some((prefix) => safe.startsWith(prefix))) {
1225
- throw new Error(`Path not allowed. Must be under: ${ALLOWED_LOG_PREFIXES.join(", ")}`);
1226
- }
1227
- }
1228
2052
  async function discoverSiteDatabases(conn, proxy) {
1229
2053
  const script = `
1230
2054
  check_dir() {
@@ -1390,17 +2214,6 @@ var TOOLS = [
1390
2214
  description: "List all SSH servers you have access to. Returns id, name, hostname, and tags for each server.",
1391
2215
  inputSchema: { type: "object", properties: {}, required: [] }
1392
2216
  },
1393
- {
1394
- name: "server-status",
1395
- description: "Get server status including uptime, disk usage, memory, and load average.",
1396
- inputSchema: {
1397
- type: "object",
1398
- properties: {
1399
- serverId: { type: "string", description: "UUID of the SSH server" }
1400
- },
1401
- required: ["serverId"]
1402
- }
1403
- },
1404
2217
  {
1405
2218
  name: "ssh-execute",
1406
2219
  description: "Execute a shell command on a remote server via SSH. Some dangerous commands are blocked for safety.",
@@ -1414,29 +2227,6 @@ var TOOLS = [
1414
2227
  required: ["serverId", "command"]
1415
2228
  }
1416
2229
  },
1417
- {
1418
- name: "server-reboot",
1419
- description: "Reboot a remote server. This will cause downtime. The server will be unavailable until it restarts.",
1420
- inputSchema: {
1421
- type: "object",
1422
- properties: {
1423
- serverId: { type: "string", description: "UUID of the SSH server to reboot" }
1424
- },
1425
- required: ["serverId"]
1426
- }
1427
- },
1428
- {
1429
- name: "server-restart-service",
1430
- description: "Restart a systemd service on a remote server using systemctl restart.",
1431
- inputSchema: {
1432
- type: "object",
1433
- properties: {
1434
- serverId: { type: "string", description: "UUID of the SSH server" },
1435
- serviceName: { type: "string", description: "Name of the systemd service (e.g. nginx, docker, lsws)" }
1436
- },
1437
- required: ["serverId", "serviceName"]
1438
- }
1439
- },
1440
2230
  {
1441
2231
  name: "sftp-list",
1442
2232
  description: "List files and directories at a given path on a remote server via SFTP.",
@@ -1497,19 +2287,6 @@ var TOOLS = [
1497
2287
  required: ["serverId"]
1498
2288
  }
1499
2289
  },
1500
- {
1501
- name: "docker-action",
1502
- description: "Perform an action on a Docker container: start, stop, restart, or remove.",
1503
- inputSchema: {
1504
- type: "object",
1505
- properties: {
1506
- serverId: { type: "string", description: "UUID of the SSH server" },
1507
- containerName: { type: "string", description: "Container name or ID" },
1508
- action: { type: "string", enum: ["start", "stop", "restart", "remove"], description: "Action to perform" }
1509
- },
1510
- required: ["serverId", "containerName", "action"]
1511
- }
1512
- },
1513
2290
  {
1514
2291
  name: "docker-logs",
1515
2292
  description: "Get recent logs from a Docker container.",
@@ -1621,86 +2398,6 @@ var TOOLS = [
1621
2398
  required: ["serverId"]
1622
2399
  }
1623
2400
  },
1624
- {
1625
- name: "log-list",
1626
- description: "Discover available log files on a server. Scans LiteSpeed logs, PHP error logs, syslog, and per-site application logs (WordPress debug.log, PrestaShop var/logs, Laravel storage/logs). Returns paths with file sizes and last modified dates.",
1627
- inputSchema: {
1628
- type: "object",
1629
- properties: {
1630
- serverId: { type: "string", description: "UUID of the SSH server" }
1631
- },
1632
- required: ["serverId"]
1633
- }
1634
- },
1635
- {
1636
- name: "log-read",
1637
- description: "Read the last N lines from a specific log file on a server. Use log-list first to discover available files. Optionally filter lines with a grep pattern.",
1638
- inputSchema: {
1639
- type: "object",
1640
- properties: {
1641
- serverId: { type: "string", description: "UUID of the SSH server" },
1642
- path: { type: "string", description: "Absolute path to the log file (must end in .log)" },
1643
- lines: { type: "number", description: "Number of lines to read (default: 100, max: 500)" },
1644
- filter: { type: "string", description: "Optional grep pattern to filter lines before tailing" }
1645
- },
1646
- required: ["serverId", "path"]
1647
- }
1648
- },
1649
- // ----- Cron Jobs -----
1650
- {
1651
- name: "cron-list",
1652
- description: "List all cron jobs on a server. Shows root crontab, system cron directories (/etc/cron.d/), and www-data crontab. Each entry shows the schedule, command, user, and source. Disabled (commented) entries are marked.",
1653
- inputSchema: {
1654
- type: "object",
1655
- properties: {
1656
- serverId: { type: "string", description: "UUID of the SSH server" },
1657
- user: { type: "string", description: "Specific user crontab to list (default: lists root + www-data + /etc/cron.d/)" }
1658
- },
1659
- required: ["serverId"]
1660
- }
1661
- },
1662
- {
1663
- name: "cron-add",
1664
- description: 'Add a new cron job to a user crontab. Provide a standard cron schedule expression and command. Examples: "0 2 * * * /usr/bin/backup.sh" (daily at 2am), "*/5 * * * * curl http://example.com/cron.php" (every 5 min).',
1665
- inputSchema: {
1666
- type: "object",
1667
- properties: {
1668
- serverId: { type: "string", description: "UUID of the SSH server" },
1669
- schedule: { type: "string", description: 'Cron schedule expression (e.g. "0 2 * * *" or "@daily")' },
1670
- command: { type: "string", description: "Command to execute" },
1671
- user: { type: "string", description: "User whose crontab to edit (default: root)" },
1672
- comment: { type: "string", description: "Optional comment to add above the cron entry (for identification)" }
1673
- },
1674
- required: ["serverId", "schedule", "command"]
1675
- }
1676
- },
1677
- {
1678
- name: "cron-remove",
1679
- description: "Remove a cron job from a user crontab by matching the command string (exact or partial match). Use cron-list first to see existing jobs.",
1680
- inputSchema: {
1681
- type: "object",
1682
- properties: {
1683
- serverId: { type: "string", description: "UUID of the SSH server" },
1684
- commandMatch: { type: "string", description: "Full or partial command string to match for removal" },
1685
- user: { type: "string", description: "User whose crontab to edit (default: root)" }
1686
- },
1687
- required: ["serverId", "commandMatch"]
1688
- }
1689
- },
1690
- {
1691
- name: "cron-toggle",
1692
- description: "Enable or disable a cron job by commenting/uncommenting it. Matches by command string. Use cron-list to find the job first.",
1693
- inputSchema: {
1694
- type: "object",
1695
- properties: {
1696
- serverId: { type: "string", description: "UUID of the SSH server" },
1697
- commandMatch: { type: "string", description: "Full or partial command string to match" },
1698
- enable: { type: "boolean", description: "true to enable (uncomment), false to disable (comment out)" },
1699
- user: { type: "string", description: "User whose crontab to edit (default: root)" }
1700
- },
1701
- required: ["serverId", "commandMatch", "enable"]
1702
- }
1703
- },
1704
2401
  // ----- Domains (mijn.host) -----
1705
2402
  {
1706
2403
  name: "domain-list",
@@ -1775,13 +2472,14 @@ var TOOLS = [
1775
2472
  }
1776
2473
  },
1777
2474
  // ----- Trigger.dev -----
1778
- ...TRIGGER_TOOLS
2475
+ ...TRIGGER_TOOLS,
2476
+ // ----- Scripts -----
2477
+ ...SCRIPT_TOOLS,
2478
+ // ----- Agent Reporting -----
2479
+ ...AGENT_TOOLS
1779
2480
  ];
1780
- var server = new Server(
1781
- { name: "mg-dashboard-mcp", version: "2.2.0" },
1782
- { capabilities: { tools: {} } }
1783
- );
1784
- server.setRequestHandler(ListToolsRequestSchema, async () => {
2481
+ var MCP_VERSION = "2.3.1";
2482
+ async function handleListTools() {
1785
2483
  if (!authContext) return { tools: TOOLS };
1786
2484
  const accessible = TOOLS.filter((tool) => {
1787
2485
  const requiredModule = TOOL_MODULE_MAP[tool.name];
@@ -1789,8 +2487,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1789
2487
  return authContext.permissions.modules[requiredModule] === true;
1790
2488
  });
1791
2489
  return { tools: accessible };
1792
- });
1793
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2490
+ }
2491
+ async function handleCallTool(request) {
1794
2492
  if (!authContext) {
1795
2493
  return { content: [{ type: "text", text: "Error: not authenticated" }] };
1796
2494
  }
@@ -1821,15 +2519,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1821
2519
  });
1822
2520
  return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
1823
2521
  }
1824
- case "server-status": {
1825
- const { conn, proxy } = await getServerConnection(String(a.serverId));
1826
- const cmd = 'echo "=== UPTIME ===" && uptime && echo "=== DISK ===" && df -h --total && echo "=== MEMORY ===" && free -h && echo "=== LOAD ===" && cat /proc/loadavg';
1827
- const result = await sshExec(conn, cmd, proxy);
1828
- const output = result.exitCode === 0 ? result.stdout : `Exit ${result.exitCode}
1829
- ${result.stdout}
1830
- ${result.stderr}`;
1831
- return { content: [{ type: "text", text: output }] };
1832
- }
1833
2522
  // ----- SSH -----
1834
2523
  case "ssh-execute": {
1835
2524
  const command = String(a.command);
@@ -1844,21 +2533,6 @@ ${result.stdout}`);
1844
2533
  ${result.stderr}`);
1845
2534
  return { content: [{ type: "text", text: output.join("\n") }] };
1846
2535
  }
1847
- case "server-reboot": {
1848
- const { conn, proxy } = await getServerConnection(String(a.serverId));
1849
- const result = await sshExec(conn, "sudo reboot", proxy);
1850
- return { content: [{ type: "text", text: result.exitCode === 0 ? "Reboot command sent. Server will be unavailable shortly." : `Reboot failed: ${result.stderr}` }] };
1851
- }
1852
- case "server-restart-service": {
1853
- const service = String(a.serviceName).replace(/[^a-zA-Z0-9._@-]/g, "");
1854
- const { conn, proxy } = await getServerConnection(String(a.serverId));
1855
- const result = await sshExec(conn, `sudo systemctl restart ${service}`, proxy);
1856
- if (result.exitCode === 0) {
1857
- const status = await sshExec(conn, `sudo systemctl is-active ${service}`, proxy);
1858
- return { content: [{ type: "text", text: `Service "${service}" restarted. Status: ${status.stdout.trim()}` }] };
1859
- }
1860
- return { content: [{ type: "text", text: `Failed to restart "${service}": ${result.stderr}` }] };
1861
- }
1862
2536
  // ----- SFTP (no proxy support yet — direct connection only) -----
1863
2537
  case "sftp-list": {
1864
2538
  const { conn } = await getServerConnection(String(a.serverId));
@@ -1886,17 +2560,6 @@ ${result.stderr}`);
1886
2560
  const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"', proxy);
1887
2561
  return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
1888
2562
  }
1889
- case "docker-action": {
1890
- const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
1891
- const action = String(a.action);
1892
- if (!["start", "stop", "restart", "remove"].includes(action)) {
1893
- throw new Error(`Invalid action: ${action}. Use start, stop, restart, or remove.`);
1894
- }
1895
- const { conn, proxy } = await getServerConnection(String(a.serverId));
1896
- const dockerCmd = action === "remove" ? `docker rm -f ${container}` : `docker ${action} ${container}`;
1897
- const result = await sshExec(conn, dockerCmd, proxy);
1898
- return { content: [{ type: "text", text: result.exitCode === 0 ? `Container "${container}" ${action}ed successfully` : `Error: ${result.stderr}` }] };
1899
- }
1900
2563
  case "docker-logs": {
1901
2564
  const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
1902
2565
  const lines = Number(a.lines) || 100;
@@ -2120,157 +2783,6 @@ echo -e "$R"
2120
2783
  const output = (result.stdout || "").trim();
2121
2784
  return { content: [{ type: "text", text: output || "Cache purge completed (no output)" }] };
2122
2785
  }
2123
- // ----- Log Reading -----
2124
- case "log-list": {
2125
- const { conn, proxy } = await getServerConnection(String(a.serverId));
2126
- const script = `
2127
- {
2128
- [ -d /usr/local/lsws/logs ] && find /usr/local/lsws/logs -maxdepth 2 -name "*.log" -type f 2>/dev/null
2129
- for f in /var/log/syslog /var/log/messages /var/log/auth.log /var/log/kern.log; do [ -f "$f" ] && echo "$f"; done
2130
- find /var/log -maxdepth 2 \\( -name "php*.log" -o -name "lsphp*.log" \\) -type f 2>/dev/null
2131
- [ -d /var/log/mg-monitoring ] && find /var/log/mg-monitoring -maxdepth 1 -name "*.log" -type f 2>/dev/null
2132
- for dir in /var/www/*/; do
2133
- [ -d "$dir" ] || continue
2134
- for root in "$dir" "\${dir}html" "\${dir}public_html" "\${dir}public" "\${dir}httpdocs"; do
2135
- [ -d "$root" ] || continue
2136
- [ -f "$root/wp-content/debug.log" ] && echo "$root/wp-content/debug.log"
2137
- [ -d "$root/var/logs" ] && find "$root/var/logs" -maxdepth 1 -name "*.log" -type f 2>/dev/null
2138
- [ -d "$root/log" ] && find "$root/log" -maxdepth 1 -name "*.log" -type f 2>/dev/null
2139
- [ -d "$root/storage/logs" ] && find "$root/storage/logs" -maxdepth 1 -name "*.log" -type f 2>/dev/null
2140
- done
2141
- done
2142
- } | sort -u | while IFS= read -r f; do
2143
- SZ=$(stat -c%s "$f" 2>/dev/null || echo 0)
2144
- MOD=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
2145
- if [ "$SZ" -ge 1048576 ] 2>/dev/null; then HR="$(( SZ / 1048576 ))MB"
2146
- elif [ "$SZ" -ge 1024 ] 2>/dev/null; then HR="$(( SZ / 1024 ))KB"
2147
- else HR="\${SZ}B"; fi
2148
- echo "$f $HR $MOD"
2149
- done
2150
- `.trim();
2151
- const result = await sshExec(conn, script, proxy);
2152
- return { content: [{ type: "text", text: result.stdout || "No log files found" }] };
2153
- }
2154
- case "log-read": {
2155
- const logPath = sanitizePath(String(a.path));
2156
- assertAllowedLogPath(logPath);
2157
- const lineCount = Math.min(Math.max(Number(a.lines) || 100, 1), 500);
2158
- const filter = a.filter ? String(a.filter).replace(/'/g, "'\\''") : "";
2159
- const { conn, proxy } = await getServerConnection(String(a.serverId));
2160
- const cmd = filter ? `grep '${filter}' '${logPath}' 2>/dev/null | tail -n ${lineCount}` : `tail -n ${lineCount} '${logPath}' 2>/dev/null`;
2161
- const result = await sshExec(conn, cmd, proxy);
2162
- if (result.exitCode !== 0 && !result.stdout) {
2163
- throw new Error(result.stderr || `Failed to read log: ${logPath}`);
2164
- }
2165
- return { content: [{ type: "text", text: result.stdout || "(empty log file)" }] };
2166
- }
2167
- // ----- Cron Jobs -----
2168
- case "cron-list": {
2169
- const { conn, proxy } = await getServerConnection(a.serverId);
2170
- const user = a.user ? String(a.user) : null;
2171
- const parts = [];
2172
- if (user) {
2173
- const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`, proxy);
2174
- parts.push(`## Crontab for ${user}
2175
- ${result.stdout}`);
2176
- } else {
2177
- const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`, proxy);
2178
- parts.push(`## Root crontab
2179
- ${rootCron.stdout}`);
2180
- const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`, proxy);
2181
- parts.push(`## www-data crontab
2182
- ${wwwCron.stdout}`);
2183
- const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'`, proxy);
2184
- parts.push(`## /etc/cron.d/
2185
- ${cronD.stdout}`);
2186
- const summary = await sshExec(conn, [
2187
- `echo "## Scheduled directories"`,
2188
- `echo "cron.hourly: $(ls /etc/cron.hourly/ 2>/dev/null | wc -l) scripts"`,
2189
- `echo "cron.daily: $(ls /etc/cron.daily/ 2>/dev/null | wc -l) scripts"`,
2190
- `echo "cron.weekly: $(ls /etc/cron.weekly/ 2>/dev/null | wc -l) scripts"`,
2191
- `echo "cron.monthly: $(ls /etc/cron.monthly/ 2>/dev/null | wc -l) scripts"`
2192
- ].join(" && "), proxy);
2193
- parts.push(summary.stdout);
2194
- }
2195
- return { content: [{ type: "text", text: parts.join("\n\n") }] };
2196
- }
2197
- case "cron-add": {
2198
- if (!a.schedule || !a.command) {
2199
- throw new Error("Both schedule and command are required");
2200
- }
2201
- const { conn, proxy } = await getServerConnection(a.serverId);
2202
- const user = a.user ? String(a.user) : "root";
2203
- const schedule = String(a.schedule).trim();
2204
- const command = String(a.command).trim();
2205
- const commentText = a.comment ? String(a.comment).trim() : "";
2206
- const entry = commentText ? `# ${commentText}
2207
- ${schedule} ${command}` : `${schedule} ${command}`;
2208
- const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
2209
- if (result.exitCode !== 0) {
2210
- throw new Error(result.stderr || "Failed to add cron job");
2211
- }
2212
- return { content: [{ type: "text", text: `Cron job added for ${user}:
2213
- ${schedule} ${command}` }] };
2214
- }
2215
- case "cron-remove": {
2216
- if (!a.commandMatch) {
2217
- throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
2218
- }
2219
- const { conn, proxy } = await getServerConnection(a.serverId);
2220
- const user = a.user ? String(a.user) : "root";
2221
- const match = String(a.commandMatch).trim();
2222
- const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
2223
- const lines = current.stdout.split("\n");
2224
- const before = lines.length;
2225
- const filtered = lines.filter((line) => {
2226
- const trimmed = line.replace(/^#\s*/, "");
2227
- return !trimmed.includes(match);
2228
- });
2229
- const removed = before - filtered.length;
2230
- if (removed === 0) {
2231
- return { content: [{ type: "text", text: `No cron entries found matching "${match}"` }] };
2232
- }
2233
- const escapedCrontab = filtered.join("\n").replace(/'/g, "'\\''");
2234
- const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
2235
- if (result.exitCode !== 0) {
2236
- throw new Error(result.stderr || "Failed to update crontab");
2237
- }
2238
- return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
2239
- }
2240
- case "cron-toggle": {
2241
- if (!a.commandMatch) {
2242
- throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
2243
- }
2244
- const { conn, proxy } = await getServerConnection(a.serverId);
2245
- const user = a.user ? String(a.user) : "root";
2246
- const match = String(a.commandMatch).trim();
2247
- const enable = Boolean(a.enable);
2248
- const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
2249
- const lines = current.stdout.split("\n");
2250
- let toggled = 0;
2251
- const updated = lines.map((line) => {
2252
- if (enable && line.startsWith("#") && line.replace(/^#\s*/, "").includes(match)) {
2253
- toggled++;
2254
- return line.replace(/^#\s*/, "");
2255
- }
2256
- if (!enable && !line.startsWith("#") && line.includes(match)) {
2257
- toggled++;
2258
- return `# ${line}`;
2259
- }
2260
- return line;
2261
- });
2262
- if (toggled === 0) {
2263
- const state = enable ? "disabled" : "enabled";
2264
- return { content: [{ type: "text", text: `No ${state} cron entries found matching "${match}"` }] };
2265
- }
2266
- const escapedCrontab = updated.join("\n").replace(/'/g, "'\\''");
2267
- const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
2268
- if (result.exitCode !== 0) {
2269
- throw new Error(result.stderr || "Failed to update crontab");
2270
- }
2271
- const action = enable ? "Enabled" : "Disabled";
2272
- return { content: [{ type: "text", text: `${action} ${toggled} cron entry/entries matching "${match}" in ${user} crontab` }] };
2273
- }
2274
2786
  // ----- Domains (mijn.host) -----
2275
2787
  case "domain-list": {
2276
2788
  const res = await mijnhostFetch("/domains");
@@ -2412,13 +2924,29 @@ ${lines.join("\n")}` }] };
2412
2924
  if (TRIGGER_TOOL_NAMES.has(name)) {
2413
2925
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
2414
2926
  }
2927
+ if (SCRIPT_TOOL_NAMES.has(name)) {
2928
+ return handleScriptTool(name, a);
2929
+ }
2930
+ if (AGENT_TOOL_NAMES.has(name)) {
2931
+ return handleAgentTool(name, a, { supabase, workspaceId: agentWorkspaceId });
2932
+ }
2415
2933
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
2416
2934
  }
2417
2935
  } catch (err) {
2418
2936
  const message = err instanceof Error ? err.message : String(err);
2419
2937
  return { content: [{ type: "text", text: `Error: ${message}` }] };
2420
2938
  }
2421
- });
2939
+ }
2940
+ function createMcpServer() {
2941
+ const s = new Server(
2942
+ { name: "mg-dashboard-mcp", version: MCP_VERSION },
2943
+ { capabilities: { tools: {} } }
2944
+ );
2945
+ s.setRequestHandler(ListToolsRequestSchema, handleListTools);
2946
+ s.setRequestHandler(CallToolRequestSchema, handleCallTool);
2947
+ return s;
2948
+ }
2949
+ var server = createMcpServer();
2422
2950
  async function main() {
2423
2951
  console.error("Starting MG Dashboard MCP Server...");
2424
2952
  authContext = await validateApiKey(apiKey);
@@ -2426,10 +2954,71 @@ async function main() {
2426
2954
  console.error("API key validation failed");
2427
2955
  process.exit(1);
2428
2956
  }
2429
- console.error("API key validated. Starting stdio transport...");
2430
- const transport = new StdioServerTransport();
2431
- await server.connect(transport);
2432
- console.error("MCP Server ready. Tools available: " + TOOLS.map((t) => t.name).join(", "));
2957
+ const toolNames = TOOLS.map((t) => t.name).join(", ");
2958
+ if (httpMode) {
2959
+ console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
2960
+ const transports = /* @__PURE__ */ new Map();
2961
+ const httpServer = createServer(async (req, res) => {
2962
+ const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
2963
+ if (url.pathname !== "/mcp") {
2964
+ res.writeHead(404, { "Content-Type": "text/plain" });
2965
+ res.end("Not found");
2966
+ return;
2967
+ }
2968
+ if (req.method === "OPTIONS") {
2969
+ res.writeHead(204, {
2970
+ "Access-Control-Allow-Origin": "*",
2971
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
2972
+ "Access-Control-Allow-Headers": "Content-Type, mcp-session-id",
2973
+ "Access-Control-Expose-Headers": "mcp-session-id"
2974
+ });
2975
+ res.end();
2976
+ return;
2977
+ }
2978
+ res.setHeader("Access-Control-Allow-Origin", "*");
2979
+ res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
2980
+ const sessionId = req.headers["mcp-session-id"];
2981
+ let body;
2982
+ if (req.method === "POST") {
2983
+ const chunks = [];
2984
+ for await (const chunk of req) chunks.push(chunk);
2985
+ try {
2986
+ body = JSON.parse(Buffer.concat(chunks).toString());
2987
+ } catch {
2988
+ res.writeHead(400, { "Content-Type": "text/plain" });
2989
+ res.end("Invalid JSON");
2990
+ return;
2991
+ }
2992
+ }
2993
+ if (sessionId && transports.has(sessionId)) {
2994
+ await transports.get(sessionId).handleRequest(req, res, body);
2995
+ return;
2996
+ }
2997
+ if (req.method === "POST" && body && (Array.isArray(body) ? body.some(isInitializeRequest) : isInitializeRequest(body))) {
2998
+ const transport = new StreamableHTTPServerTransport({
2999
+ sessionIdGenerator: () => randomUUID()
3000
+ });
3001
+ transport.onclose = () => {
3002
+ if (transport.sessionId) transports.delete(transport.sessionId);
3003
+ };
3004
+ const sessionServer = createMcpServer();
3005
+ await sessionServer.connect(transport);
3006
+ if (transport.sessionId) transports.set(transport.sessionId, transport);
3007
+ await transport.handleRequest(req, res, body);
3008
+ return;
3009
+ }
3010
+ res.writeHead(400, { "Content-Type": "text/plain" });
3011
+ res.end("Bad request \u2014 missing or invalid session");
3012
+ });
3013
+ httpServer.listen(httpPort, () => {
3014
+ console.error(`MCP HTTP server ready on port ${httpPort}. Tools: ${toolNames}`);
3015
+ });
3016
+ } else {
3017
+ console.error("API key validated. Starting stdio transport...");
3018
+ const transport = new StdioServerTransport();
3019
+ await server.connect(transport);
3020
+ console.error(`MCP Server ready. Tools: ${toolNames}`);
3021
+ }
2433
3022
  }
2434
3023
  main().catch((err) => {
2435
3024
  console.error("Fatal error:", err);