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