@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 +642 -48
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +642 -55
- package/dist/server.mjs.map +1 -1
- package/package.json +17 -12
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 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(` -
|
|
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"`);
|
|
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(` -
|
|
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"'];
|
|
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
|
-
`
|
|
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(`
|
|
544
|
-
yamlLines.push(`
|
|
545
|
-
yamlLines.push(`
|
|
546
|
-
yamlLines.push(`
|
|
547
|
-
yamlLines.push(`
|
|
548
|
-
yamlLines.push(`
|
|
549
|
-
yamlLines.push(`
|
|
550
|
-
yamlLines.push(`
|
|
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:
|
|
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);
|