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