@stackwright-pro/mcp 0.1.1 → 0.2.0-alpha.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/server.js CHANGED
@@ -39,8 +39,8 @@ function registerDataExplorerTools(server2) {
39
39
  },
40
40
  async ({ specPath, projectRoot }) => {
41
41
  const result = (0, import_cli_data_explorer.listEntities)({
42
- specPath,
43
- projectRoot
42
+ ...specPath !== void 0 && { specPath },
43
+ ...projectRoot !== void 0 && { projectRoot }
44
44
  });
45
45
  if (!result.success) {
46
46
  return {
@@ -90,8 +90,8 @@ function registerDataExplorerTools(server2) {
90
90
  async ({ selectedEntities, excludePatterns, projectRoot }) => {
91
91
  const result = (0, import_cli_data_explorer.generateFilter)({
92
92
  selectedEntities,
93
- excludePatterns,
94
- projectRoot: projectRoot || process.cwd()
93
+ ...excludePatterns !== void 0 && { excludePatterns },
94
+ projectRoot: projectRoot ?? process.cwd()
95
95
  });
96
96
  if (!result.success) {
97
97
  return {
@@ -111,8 +111,8 @@ function registerDataExplorerTools(server2) {
111
111
  lines.push(" endpoints:");
112
112
  if (result.filter.include && result.filter.include.length > 0) {
113
113
  lines.push(" include:");
114
- for (const path2 of result.filter.include) {
115
- lines.push(` - ${path2}`);
114
+ for (const path3 of result.filter.include) {
115
+ lines.push(` - ${path3}`);
116
116
  }
117
117
  }
118
118
  if (result.filter.exclude && result.filter.exclude.length > 0) {
@@ -142,6 +142,7 @@ function registerDataExplorerTools(server2) {
142
142
 
143
143
  // src/tools/security.ts
144
144
  var import_zod2 = require("zod");
145
+ var import_crypto = require("crypto");
145
146
  var import_fs = __toESM(require("fs"));
146
147
  var import_path = __toESM(require("path"));
147
148
  function registerSecurityTools(server2) {
@@ -154,7 +155,7 @@ function registerSecurityTools(server2) {
154
155
  },
155
156
  async ({ specPath, configPath }) => {
156
157
  let securityEnabled = false;
157
- let allowlist = [];
158
+ const allowlist = [];
158
159
  const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
159
160
  if (import_fs.default.existsSync(configFile)) {
160
161
  try {
@@ -219,8 +220,7 @@ This spec is not on the approved-specs allowlist. Add it using stackwright_pro_a
219
220
  if (!specPath.startsWith("http://") && !specPath.startsWith("https://")) {
220
221
  if (import_fs.default.existsSync(specPath)) {
221
222
  const specContent = import_fs.default.readFileSync(specPath, "utf8");
222
- const crypto = require("crypto");
223
- const sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
223
+ const sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
224
224
  if (sha256 !== matchingSpec.sha256) {
225
225
  return {
226
226
  content: [
@@ -271,8 +271,7 @@ Status: Valid (${allowlist.length} specs on allowlist)`
271
271
  sha256 = "<computed-at-build>";
272
272
  } else if (import_fs.default.existsSync(url)) {
273
273
  const specContent = import_fs.default.readFileSync(url, "utf8");
274
- const crypto = require("crypto");
275
- sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
274
+ sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
276
275
  } else {
277
276
  return {
278
277
  content: [
@@ -477,7 +476,7 @@ var import_cli_data_explorer2 = require("@stackwright-pro/cli-data-explorer");
477
476
  function registerDashboardTools(server2) {
478
477
  server2.tool(
479
478
  "stackwright_pro_generate_dashboard",
480
- "Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with collection_listing, data_table, or stats_grid content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
479
+ "Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with grid, metric_card, data_table, and collection_list content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
481
480
  {
482
481
  entities: import_zod4.z.array(import_zod4.z.string()).describe("Entity slugs to include in dashboard"),
483
482
  layout: import_zod4.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
@@ -506,48 +505,44 @@ function registerDashboardTools(server2) {
506
505
  ];
507
506
  if (layout === "grid" || layout === "mixed") {
508
507
  for (const slug of entities) {
509
- yamlLines.push(` - stats_grid:`);
510
- yamlLines.push(` label: "${slug}-stats"`);
511
- yamlLines.push(` heading:`);
512
- yamlLines.push(
513
- ` text: "${slug.charAt(0).toUpperCase() + slug.slice(1)} Overview"`
514
- );
515
- yamlLines.push(` textSize: "h2"`);
516
- yamlLines.push(` stats:`);
517
- yamlLines.push(` - label: "Total"`);
518
- yamlLines.push(` value: "{{ ${slug}.count }}"`);
519
- yamlLines.push(` icon: "Database"`);
520
- yamlLines.push(` source: "${slug}"`);
521
- yamlLines.push(` background: "surface"`);
508
+ yamlLines.push(` - type: grid`);
509
+ yamlLines.push(` columns: 4`);
510
+ yamlLines.push(` items:`);
511
+ yamlLines.push(` - type: metric_card`);
512
+ yamlLines.push(` label: "Total"`);
513
+ yamlLines.push(` value: {{ ${slug}.count }}`);
514
+ yamlLines.push(` icon: Database`);
515
+ yamlLines.push(` - type: metric_card`);
516
+ yamlLines.push(` label: "Active"`);
517
+ yamlLines.push(` value: 0`);
518
+ yamlLines.push(` icon: CheckCircle`);
522
519
  yamlLines.push("");
523
520
  }
524
521
  }
525
- yamlLines.push(` - collection_listing:`);
526
- yamlLines.push(` label: "${entities[0]}-listing"`);
527
- yamlLines.push(` heading:`);
528
- yamlLines.push(
529
- ` text: "${entities.map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" & ")}"`
530
- );
531
- yamlLines.push(` textSize: "h2"`);
532
- yamlLines.push(` collection: "${entities[0]}"`);
533
- yamlLines.push(` showFilters: true`);
534
- yamlLines.push(` showSearch: true`);
535
- const defaultCols = ['"id"', '"name"', '"status"', '"created_at"'];
522
+ yamlLines.push(` - type: collection_list`);
523
+ yamlLines.push(` label: ${entities[0]}-listing`);
524
+ yamlLines.push(` collection: ${entities[0]}`);
525
+ yamlLines.push(` showFilters: true`);
526
+ yamlLines.push(` showSearch: true`);
527
+ const defaultCols = ["id", "name", "status", "created_at"];
536
528
  yamlLines.push(
537
- ` columns: ${entityDetails[0]?.fields?.slice(0, 4).map((f) => `"${f.name}"`).join(", ") || defaultCols.join(", ")}`
529
+ ` columns: ${entityDetails[0]?.fields?.slice(0, 4).map((f) => f.name).join(", ") || defaultCols.join(", ")}`
538
530
  );
539
- yamlLines.push(` background: "background"`);
540
531
  yamlLines.push("");
541
532
  if (layout === "table") {
542
- yamlLines.push(` - data_table:`);
543
- yamlLines.push(` label: "${entities[0]}-table"`);
544
- yamlLines.push(` heading:`);
545
- yamlLines.push(` text: "Detailed View"`);
546
- yamlLines.push(` textSize: "h3"`);
547
- yamlLines.push(` collection: "${entities[0]}"`);
548
- yamlLines.push(` sortableColumns: true`);
549
- yamlLines.push(` exportable: true`);
550
- yamlLines.push(` background: "surface"`);
533
+ yamlLines.push(` - type: data_table`);
534
+ yamlLines.push(` label: ${entities[0]}-table`);
535
+ yamlLines.push(` collection: ${entities[0]}`);
536
+ yamlLines.push(` columns:`);
537
+ yamlLines.push(` - field: id`);
538
+ yamlLines.push(` header: ID`);
539
+ yamlLines.push(` sortable: true`);
540
+ yamlLines.push(` - field: name`);
541
+ yamlLines.push(` header: Name`);
542
+ yamlLines.push(` sortable: true`);
543
+ yamlLines.push(` - field: status`);
544
+ yamlLines.push(` header: Status`);
545
+ yamlLines.push(` type: badge`);
551
546
  }
552
547
  const yaml = yamlLines.join("\n");
553
548
  return {
@@ -649,15 +644,614 @@ ${yaml}
649
644
  );
650
645
  }
651
646
 
647
+ // src/tools/clarification.ts
648
+ var import_zod5 = require("zod");
649
+ var import_child_process = require("child_process");
650
+ var import_os = require("os");
651
+ var import_path2 = require("path");
652
+ var import_crypto2 = require("crypto");
653
+ var import_fs2 = require("fs");
654
+ var activeServers = /* @__PURE__ */ new Map();
655
+ async function startPythonServer(sessionId) {
656
+ const socketPath = (0, import_path2.join)((0, import_os.tmpdir)(), `otter-raft-${(0, import_crypto2.randomUUID)()}.sock`);
657
+ const port = 8765 + Math.floor(Math.random() * 100);
658
+ return new Promise((resolve, reject) => {
659
+ const pythonPath = process.platform === "win32" ? "python" : "python3";
660
+ const packageRoot = findPythonPackageRoot();
661
+ const proc = (0, import_child_process.spawn)(
662
+ pythonPath,
663
+ ["-m", "stackwright_pro.raft.server", "--socket", socketPath, "--port", String(port)],
664
+ {
665
+ stdio: ["pipe", "pipe", "pipe"],
666
+ env: {
667
+ ...process.env,
668
+ PYTHONPATH: packageRoot
669
+ }
670
+ }
671
+ );
672
+ activeServers.set(sessionId, proc);
673
+ let startupOutput = "";
674
+ let started = false;
675
+ proc.stdout?.on("data", (data) => {
676
+ startupOutput += data.toString();
677
+ if (startupOutput.includes("Server ready") || startupOutput.includes("HTTP server")) {
678
+ started = true;
679
+ resolve({ port, socketPath });
680
+ }
681
+ });
682
+ proc.stderr?.on("data", (data) => {
683
+ if (!startupOutput.includes("Starting")) {
684
+ console.error("[Python Clarification]", data.toString().trim());
685
+ }
686
+ });
687
+ proc.on("error", (err) => {
688
+ if (!started) {
689
+ activeServers.delete(sessionId);
690
+ reject(new Error(`Failed to start Python server: ${err.message}`));
691
+ }
692
+ });
693
+ proc.on("exit", (code) => {
694
+ activeServers.delete(sessionId);
695
+ if ((0, import_fs2.existsSync)(socketPath)) {
696
+ try {
697
+ (0, import_fs2.unlinkSync)(socketPath);
698
+ } catch {
699
+ }
700
+ }
701
+ });
702
+ setTimeout(() => {
703
+ if (!started) {
704
+ proc.kill();
705
+ activeServers.delete(sessionId);
706
+ reject(new Error("Python server startup timeout"));
707
+ }
708
+ }, 1e4);
709
+ });
710
+ }
711
+ async function stopPythonServer(sessionId) {
712
+ const proc = activeServers.get(sessionId);
713
+ if (proc) {
714
+ proc.kill("SIGTERM");
715
+ activeServers.delete(sessionId);
716
+ }
717
+ }
718
+ async function httpRequest(host, port, path3, body) {
719
+ const http = await import("http");
720
+ return new Promise((resolve, reject) => {
721
+ const url = new URL(path3, `http://${host}:${port}`);
722
+ const reqOptions = {
723
+ hostname: host,
724
+ port,
725
+ path: url.pathname,
726
+ method: body ? "POST" : "GET",
727
+ headers: {
728
+ "Content-Type": "application/json"
729
+ }
730
+ };
731
+ const req = http.request(reqOptions, (res) => {
732
+ let data = "";
733
+ res.on("data", (chunk) => {
734
+ data += chunk.toString();
735
+ });
736
+ res.on("end", () => {
737
+ try {
738
+ const parsed = JSON.parse(data);
739
+ if (res.statusCode && res.statusCode >= 400) {
740
+ reject(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
741
+ } else {
742
+ resolve(parsed);
743
+ }
744
+ } catch (e) {
745
+ reject(new Error(`Failed to parse response: ${data}`));
746
+ }
747
+ });
748
+ });
749
+ req.on("error", reject);
750
+ if (body) {
751
+ req.write(JSON.stringify(body));
752
+ }
753
+ req.end();
754
+ });
755
+ }
756
+ function findPythonPackageRoot() {
757
+ const candidates = [
758
+ (0, import_path2.join)(__dirname, "../../python/src"),
759
+ (0, import_path2.join)(__dirname, "../../../python/src"),
760
+ (0, import_path2.join)(process.cwd(), "python/src")
761
+ ];
762
+ for (const candidate of candidates) {
763
+ if ((0, import_fs2.existsSync)(candidate)) {
764
+ return candidate;
765
+ }
766
+ }
767
+ return process.cwd();
768
+ }
769
+ function registerClarificationTools(server2) {
770
+ server2.tool(
771
+ "stackwright_pro_clarify",
772
+ "Ask the user for clarification when an otter encounters ambiguity. This is for MID-EXECUTION questions, not upfront question collection. Use this when the otter needs user input to proceed. Returns the user's decision which should be used to continue execution.",
773
+ {
774
+ context: import_zod5.z.string().optional().describe("Context about what the otter is trying to do"),
775
+ question_type: import_zod5.z.enum(["closed_choice", "open_text", "conditional", "multi_step", "reconciliation"]).describe("Type of question being asked"),
776
+ question: import_zod5.z.string().describe("The clarification question to ask the user"),
777
+ choices: import_zod5.z.array(import_zod5.z.string()).optional().describe("Options for closed_choice or multi_step questions"),
778
+ priority: import_zod5.z.enum(["blocking", "preferred", "optional"]).optional().describe("How critical is this clarification? Default: preferred"),
779
+ target_field: import_zod5.z.string().optional().describe("What field/config does this clarify?")
780
+ },
781
+ async ({ context, question_type, question, choices, priority = "preferred", target_field }) => {
782
+ const sessionId = `mcp_${(0, import_crypto2.randomUUID)().slice(0, 8)}`;
783
+ try {
784
+ const { port } = await startPythonServer(sessionId);
785
+ await httpRequest(port === 8765 ? "127.0.0.1" : "127.0.0.1", port, "/sessions", {});
786
+ if (context) {
787
+ await httpRequest(
788
+ port === 8765 ? "127.0.0.1" : "127.0.0.1",
789
+ port,
790
+ `/sessions/${sessionId}/context`,
791
+ { context: { purpose: context } }
792
+ );
793
+ }
794
+ const request = {
795
+ ...context !== void 0 && { context },
796
+ question_type,
797
+ question,
798
+ ...choices !== void 0 && { choices },
799
+ priority,
800
+ ...target_field !== void 0 && { target_field }
801
+ };
802
+ const response = await httpRequest(
803
+ port === 8765 ? "127.0.0.1" : "127.0.0.1",
804
+ port,
805
+ "/clarify",
806
+ { request }
807
+ );
808
+ await stopPythonServer(sessionId);
809
+ const decision = response.decision;
810
+ const value = decision.value;
811
+ const source = decision.source;
812
+ const explicit = decision.explicit ? "explicitly" : "via fallback";
813
+ if (response.fallback_used) {
814
+ return {
815
+ content: [
816
+ {
817
+ type: "text",
818
+ text: `\u26A0\uFE0F Clarification fallback used: ${response.fallback_reason || "No user input available"}
819
+
820
+ Default value used: ${JSON.stringify(value)}
821
+
822
+ \u{1F4A1} Consider following up with the user later if this default isn't appropriate.`
823
+ }
824
+ ]
825
+ };
826
+ }
827
+ return {
828
+ content: [
829
+ {
830
+ type: "text",
831
+ text: `\u2705 User clarified (${source}): ${JSON.stringify(value)}
832
+
833
+ Use this value to continue execution.`
834
+ }
835
+ ]
836
+ };
837
+ } catch (error) {
838
+ await stopPythonServer(sessionId);
839
+ return {
840
+ content: [
841
+ {
842
+ type: "text",
843
+ text: `\u274C Clarification failed: ${error instanceof Error ? error.message : "Unknown error"}
844
+
845
+ Cannot proceed without user input. Consider:
846
+ 1. Using a reasonable default
847
+ 2. Asking the user directly in your response
848
+ 3. Skipping this step if optional`
849
+ }
850
+ ],
851
+ isError: true
852
+ };
853
+ }
854
+ }
855
+ );
856
+ server2.tool(
857
+ "stackwright_pro_detect_conflict",
858
+ "Detect when a user's stated preference conflicts with their selected choices. Use this to identify when users might be indecisive or misunderstood a question. Returns conflict details and resolution options.",
859
+ {
860
+ stated_preference: import_zod5.z.string().describe("What the user said they wanted"),
861
+ selected_values: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).describe("What the user actually selected")
862
+ },
863
+ async ({ stated_preference, selected_values }) => {
864
+ const sessionId = `mcp_${(0, import_crypto2.randomUUID)().slice(0, 8)}`;
865
+ try {
866
+ const { port } = await startPythonServer(sessionId);
867
+ const response = await httpRequest(
868
+ port === 8765 ? "127.0.0.1" : "127.0.0.1",
869
+ port,
870
+ "/conflict",
871
+ { stated_preference, selected_values }
872
+ );
873
+ await stopPythonServer(sessionId);
874
+ if (response.conflict) {
875
+ const data = response.data;
876
+ return {
877
+ content: [
878
+ {
879
+ type: "text",
880
+ text: `\u26A0\uFE0F CONFLICT DETECTED
881
+
882
+ User stated: "${stated_preference}"
883
+ But selected: ${JSON.stringify(selected_values)}
884
+
885
+ Conflict: ${data.description}
886
+
887
+ Resolution options: ${data.options?.join(", ") || "Ask user to clarify"}
888
+
889
+ \u{1F4A1} Consider asking the user to reconcile this conflict.`
890
+ }
891
+ ]
892
+ };
893
+ }
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text: `\u2705 No conflict detected between stated preference and selections.`
899
+ }
900
+ ]
901
+ };
902
+ } catch (error) {
903
+ await stopPythonServer(sessionId);
904
+ return {
905
+ content: [
906
+ {
907
+ type: "text",
908
+ text: `\u274C Conflict detection failed: ${error instanceof Error ? error.message : "Unknown error"}`
909
+ }
910
+ ],
911
+ isError: true
912
+ };
913
+ }
914
+ }
915
+ );
916
+ server2.tool(
917
+ "stackwright_pro_get_defaults",
918
+ "Get the current clarification defaults from config. Use this to understand what fallback values will be used if user doesn't provide input.",
919
+ {
920
+ config_path: import_zod5.z.string().optional().describe("Path to config file. Default: .stackwright/clarification.yaml")
921
+ },
922
+ async ({ config_path }) => {
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: `\u{1F4CB} Clarification defaults
928
+
929
+ Configuration is loaded from:
930
+ - Environment: CLARIFICATION_* variables
931
+ - Config file: ${config_path || ".stackwright/clarification.yaml"}
932
+ - CLI args: --clarify-* flags
933
+
934
+ Default behaviors:
935
+ - allow_dont_know: true (users can skip)
936
+ - default_timeout: 120 seconds
937
+ - channel_priority: [tui, cli_args, config, defaults]
938
+
939
+ \u{1F4A1} Set CLARIFICATION_DEFAULT_<FIELD>=value to change defaults.`
940
+ }
941
+ ]
942
+ };
943
+ }
944
+ );
945
+ }
946
+
947
+ // src/tools/packages.ts
948
+ var import_zod6 = require("zod");
949
+ var import_fs3 = require("fs");
950
+ var import_child_process2 = require("child_process");
951
+ var import_path3 = __toESM(require("path"));
952
+ function registerPackageTools(server2) {
953
+ server2.tool(
954
+ "stackwright_pro_setup_packages",
955
+ "Ensures pro packages are present in a project's package.json. Safe to call multiple times \u2014 never overwrites existing version pins. Use this to bootstrap dependencies before specialist otters run.",
956
+ {
957
+ // FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
958
+ packages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).describe(
959
+ 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
960
+ ),
961
+ devPackages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("devDependencies to add. Same format as packages."),
962
+ scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("npm scripts to add. Only adds if key does not already exist."),
963
+ targetDir: import_zod6.z.string().optional().describe(
964
+ "Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
965
+ ),
966
+ runInstall: import_zod6.z.boolean().optional().default(true).describe("Run pnpm install after writing package.json. Defaults to true.")
967
+ },
968
+ async ({ packages, devPackages, scripts, targetDir, runInstall }) => {
969
+ const result = setupPackages({
970
+ packages,
971
+ runInstall,
972
+ ...devPackages !== void 0 ? { devPackages } : {},
973
+ ...scripts !== void 0 ? { scripts } : {},
974
+ ...targetDir !== void 0 ? { targetDir } : {}
975
+ });
976
+ const statusLine = result.success ? `\u2705 package.json updated: ${result.packageJsonPath}` : `\u274C Failed: ${result.error}`;
977
+ const lines = [
978
+ statusLine,
979
+ "",
980
+ result.added.length > 0 ? `Added (${result.added.length}): ${result.added.join(", ")}` : "Added: none",
981
+ result.skipped.length > 0 ? `Skipped/already present (${result.skipped.length}): ${result.skipped.join(", ")}` : "Skipped: none",
982
+ result.scriptsAdded.length > 0 ? `Scripts added (${result.scriptsAdded.length}): ${result.scriptsAdded.join(", ")}` : "Scripts added: none",
983
+ `pnpm install: ${result.installed ? "ran successfully" : result.success && runInstall ? "failed (non-fatal)" : "skipped"}`
984
+ ];
985
+ return {
986
+ content: [
987
+ {
988
+ type: "text",
989
+ text: lines.join("\n")
990
+ },
991
+ {
992
+ type: "text",
993
+ text: JSON.stringify(result)
994
+ }
995
+ ]
996
+ };
997
+ }
998
+ );
999
+ }
1000
+ function setupPackages(opts) {
1001
+ const emptyResult = {
1002
+ added: [],
1003
+ skipped: [],
1004
+ scriptsAdded: [],
1005
+ installed: false,
1006
+ packageJsonPath: ""
1007
+ };
1008
+ try {
1009
+ const cwd = process.cwd();
1010
+ const resolvedTarget = opts.targetDir ? import_path3.default.resolve(opts.targetDir) : cwd;
1011
+ const cwdWithSep = cwd.endsWith(import_path3.default.sep) ? cwd : cwd + import_path3.default.sep;
1012
+ if (resolvedTarget !== cwd && !resolvedTarget.startsWith(cwdWithSep)) {
1013
+ return {
1014
+ success: false,
1015
+ added: [],
1016
+ skipped: [],
1017
+ scriptsAdded: [],
1018
+ installed: false,
1019
+ packageJsonPath: "",
1020
+ // FIX 5 (M-4): do not leak absolute paths in error messages
1021
+ error: `Path traversal rejected: target directory is outside the allowed working directory`
1022
+ };
1023
+ }
1024
+ const preResolvePackageJsonPath = import_path3.default.join(resolvedTarget, "package.json");
1025
+ if (!(0, import_fs3.existsSync)(preResolvePackageJsonPath)) {
1026
+ return {
1027
+ success: false,
1028
+ ...emptyResult,
1029
+ error: `No package.json found in ${resolvedTarget}`
1030
+ };
1031
+ }
1032
+ let realTarget;
1033
+ try {
1034
+ realTarget = (0, import_fs3.realpathSync)(resolvedTarget);
1035
+ } catch {
1036
+ return {
1037
+ success: false,
1038
+ added: [],
1039
+ skipped: [],
1040
+ scriptsAdded: [],
1041
+ installed: false,
1042
+ packageJsonPath: "",
1043
+ error: `Could not resolve real path of target directory`
1044
+ };
1045
+ }
1046
+ const realCwd = (0, import_fs3.realpathSync)(cwd);
1047
+ const realCwdWithSep = realCwd.endsWith(import_path3.default.sep) ? realCwd : realCwd + import_path3.default.sep;
1048
+ if (realTarget !== realCwd && !realTarget.startsWith(realCwdWithSep)) {
1049
+ return {
1050
+ success: false,
1051
+ added: [],
1052
+ skipped: [],
1053
+ scriptsAdded: [],
1054
+ installed: false,
1055
+ packageJsonPath: "",
1056
+ // FIX 5 (M-4): do not leak absolute paths in error messages
1057
+ error: `Path traversal rejected: target directory resolved to a location outside the allowed working directory`
1058
+ };
1059
+ }
1060
+ const realPackageJsonPath = import_path3.default.join(realTarget, "package.json");
1061
+ const pkgStat = (0, import_fs3.lstatSync)(realPackageJsonPath);
1062
+ if (pkgStat.isSymbolicLink()) {
1063
+ return {
1064
+ ...emptyResult,
1065
+ success: false,
1066
+ error: `package.json is a symlink \u2014 refusing to read or write`
1067
+ };
1068
+ }
1069
+ const VALID_PKG_NAME_RE = /^(@[a-z0-9][a-z0-9\-._~]*\/)?[a-z0-9][a-z0-9\-._~]*$/;
1070
+ const allPkgNames = [
1071
+ ...Object.keys(opts.packages ?? {}),
1072
+ ...Object.keys(opts.devPackages ?? {})
1073
+ ];
1074
+ for (const pkg of allPkgNames) {
1075
+ if (!VALID_PKG_NAME_RE.test(pkg)) {
1076
+ return {
1077
+ ...emptyResult,
1078
+ success: false,
1079
+ error: `Invalid package name: '${pkg}' \u2014 must match npm package name specification`
1080
+ };
1081
+ }
1082
+ }
1083
+ const SAFE_VERSION_RE = /^(workspace:[*^~]?|\*|latest|next|beta|alpha|canary|rc|[~^><=*|, ]*[\d.x*][\d.x*\-+a-zA-Z.~^><=*|, ]*)$/;
1084
+ const allVersionEntries = [
1085
+ ...Object.entries(opts.packages ?? {}),
1086
+ ...Object.entries(opts.devPackages ?? {})
1087
+ ];
1088
+ for (const [pkg, version] of allVersionEntries) {
1089
+ if (!SAFE_VERSION_RE.test(version.trim())) {
1090
+ return {
1091
+ ...emptyResult,
1092
+ success: false,
1093
+ error: `Unsafe version specifier for '${pkg}': '${version}' \u2014 only semver ranges, workspace:*, and dist-tags (latest/next/beta) are permitted`
1094
+ };
1095
+ }
1096
+ }
1097
+ const BLOCKED_LIFECYCLE_KEYS = /* @__PURE__ */ new Set([
1098
+ "preinstall",
1099
+ "install",
1100
+ "postinstall",
1101
+ "prepare",
1102
+ "prepublish",
1103
+ "prepublishOnly",
1104
+ "prepack",
1105
+ "postpack",
1106
+ "dependencies"
1107
+ // pnpm hook key
1108
+ ]);
1109
+ if (opts.scripts) {
1110
+ for (const key of Object.keys(opts.scripts)) {
1111
+ if (BLOCKED_LIFECYCLE_KEYS.has(key)) {
1112
+ return {
1113
+ ...emptyResult,
1114
+ success: false,
1115
+ error: `Blocked lifecycle script key: '${key}' \u2014 writing npm lifecycle hooks is not permitted`
1116
+ };
1117
+ }
1118
+ }
1119
+ }
1120
+ const raw = (0, import_fs3.readFileSync)(realPackageJsonPath, "utf8");
1121
+ const PackageJsonSchema = import_zod6.z.object({
1122
+ // Zod v4: z.record(keySchema, valueSchema) — two-arg form required
1123
+ dependencies: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional(),
1124
+ devDependencies: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional(),
1125
+ scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional()
1126
+ }).passthrough();
1127
+ const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
1128
+ if (!schemaResult.success) {
1129
+ return {
1130
+ ...emptyResult,
1131
+ success: false,
1132
+ error: `Invalid package.json structure: ${schemaResult.error.message}`
1133
+ };
1134
+ }
1135
+ const parsed = schemaResult.data;
1136
+ const added = [];
1137
+ const skipped = [];
1138
+ const scriptsAdded = [];
1139
+ const claimedPkgs = /* @__PURE__ */ new Set([
1140
+ ...Object.keys(parsed.dependencies ?? {}),
1141
+ ...Object.keys(parsed.devDependencies ?? {})
1142
+ ]);
1143
+ parsed.dependencies = parsed.dependencies ?? {};
1144
+ for (const [pkg, version] of Object.entries(opts.packages)) {
1145
+ if (claimedPkgs.has(pkg)) {
1146
+ skipped.push(pkg);
1147
+ } else {
1148
+ parsed.dependencies[pkg] = version;
1149
+ claimedPkgs.add(pkg);
1150
+ added.push(pkg);
1151
+ }
1152
+ }
1153
+ if (opts.devPackages && Object.keys(opts.devPackages).length > 0) {
1154
+ parsed.devDependencies = parsed.devDependencies ?? {};
1155
+ for (const [pkg, version] of Object.entries(opts.devPackages)) {
1156
+ if (claimedPkgs.has(pkg)) {
1157
+ skipped.push(pkg);
1158
+ } else {
1159
+ parsed.devDependencies[pkg] = version;
1160
+ claimedPkgs.add(pkg);
1161
+ added.push(pkg);
1162
+ }
1163
+ }
1164
+ }
1165
+ if (opts.scripts && Object.keys(opts.scripts).length > 0) {
1166
+ parsed.scripts = parsed.scripts ?? {};
1167
+ for (const [key, value] of Object.entries(opts.scripts)) {
1168
+ if (parsed.scripts[key] === void 0) {
1169
+ parsed.scripts[key] = value;
1170
+ scriptsAdded.push(key);
1171
+ }
1172
+ }
1173
+ }
1174
+ (0, import_fs3.writeFileSync)(realPackageJsonPath, JSON.stringify(parsed, null, 2) + "\n");
1175
+ let installed = false;
1176
+ let installError;
1177
+ if (opts.runInstall) {
1178
+ try {
1179
+ (0, import_child_process2.execSync)("pnpm install", { cwd: realTarget, stdio: "pipe", timeout: 6e4 });
1180
+ installed = true;
1181
+ } catch (err) {
1182
+ installed = false;
1183
+ const spawnErr = err;
1184
+ installError = spawnErr.stderr?.toString().trim() || (err instanceof Error ? err.message : "unknown error");
1185
+ }
1186
+ }
1187
+ return {
1188
+ success: true,
1189
+ added,
1190
+ skipped,
1191
+ scriptsAdded,
1192
+ installed,
1193
+ packageJsonPath: realPackageJsonPath,
1194
+ ...installError !== void 0 ? { installError } : {}
1195
+ };
1196
+ } catch (err) {
1197
+ return {
1198
+ ...emptyResult,
1199
+ success: false,
1200
+ error: err instanceof Error ? err.message : String(err)
1201
+ };
1202
+ }
1203
+ }
1204
+
1205
+ // package.json
1206
+ var package_default = {
1207
+ dependencies: {
1208
+ "@modelcontextprotocol/sdk": "^1.10.0",
1209
+ "@stackwright-pro/cli-data-explorer": "workspace:*",
1210
+ zod: "^4.3.6"
1211
+ },
1212
+ devDependencies: {
1213
+ "@types/node": "^24.1.0",
1214
+ tsup: "^8.5.0",
1215
+ typescript: "^5.8.3",
1216
+ vitest: "^4.0.18"
1217
+ },
1218
+ scripts: {
1219
+ build: "tsup src/server.ts --format cjs,esm --dts --clean",
1220
+ dev: "tsup src/server.ts --format cjs,esm --dts --watch",
1221
+ start: "node dist/server.js",
1222
+ test: "vitest run",
1223
+ "test:coverage": "vitest run --coverage"
1224
+ },
1225
+ name: "@stackwright-pro/mcp",
1226
+ version: "0.2.0-alpha.0",
1227
+ description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
1228
+ license: "PROPRIETARY",
1229
+ main: "./dist/server.js",
1230
+ module: "./dist/server.mjs",
1231
+ types: "./dist/server.d.ts",
1232
+ exports: {
1233
+ ".": {
1234
+ types: "./dist/server.d.ts",
1235
+ import: "./dist/server.mjs",
1236
+ require: "./dist/server.js"
1237
+ }
1238
+ },
1239
+ files: [
1240
+ "dist"
1241
+ ]
1242
+ };
1243
+
652
1244
  // src/server.ts
653
1245
  var server = new import_mcp.McpServer({
654
1246
  name: "stackwright-pro",
655
- version: "0.1.0"
1247
+ version: package_default.version
656
1248
  });
657
1249
  registerDataExplorerTools(server);
658
1250
  registerSecurityTools(server);
659
1251
  registerIsrTools(server);
660
1252
  registerDashboardTools(server);
1253
+ registerClarificationTools(server);
1254
+ registerPackageTools(server);
661
1255
  async function main() {
662
1256
  const transport = new import_stdio.StdioServerTransport();
663
1257
  await server.connect(transport);