@stackwright-pro/mcp 0.0.0-beta-20260417191149

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.
@@ -0,0 +1,1243 @@
1
+ // src/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ // src/tools/data-explorer.ts
6
+ import { z } from "zod";
7
+ import { listEntities, generateFilter } from "@stackwright-pro/cli-data-explorer";
8
+ function registerDataExplorerTools(server2) {
9
+ server2.tool(
10
+ "stackwright_pro_list_entities",
11
+ "List all available API entities from OpenAPI specs or generated Zod schemas. Use this to discover what entities are available before generating endpoint filters. Returns entity names, endpoints, and field counts. Part of the Pro Otter Raft for building API-integrated Stackwright applications.",
12
+ {
13
+ specPath: z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
14
+ projectRoot: z.string().optional().describe("Project root directory (auto-detected if omitted)")
15
+ },
16
+ async ({ specPath, projectRoot }) => {
17
+ const result = listEntities({
18
+ ...specPath !== void 0 && { specPath },
19
+ ...projectRoot !== void 0 && { projectRoot }
20
+ });
21
+ if (!result.success) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Error discovering entities: ${(result.errors ?? []).join(", ")}`
27
+ }
28
+ ],
29
+ isError: true
30
+ };
31
+ }
32
+ const lines = [`\u{1F4E6} Found ${result.entities.length} API Entities:
33
+ `];
34
+ for (const entity of result.entities) {
35
+ lines.push(`\u{1F4E6} ${entity.name} (${entity.slug})`);
36
+ lines.push(` Endpoint: ${entity.endpoint}`);
37
+ lines.push(` Fields: ${entity.fieldCount}`);
38
+ if (entity.fields.length > 0) {
39
+ const fieldTypes = entity.fields.slice(0, 3).map((f) => `${f.name}:${f.type}`).join(", ");
40
+ lines.push(` Types: ${fieldTypes}${entity.fields.length > 3 ? "..." : ""}`);
41
+ }
42
+ lines.push("");
43
+ }
44
+ lines.push(
45
+ `
46
+ \u{1F4A1} Use stackwright_pro_generate_filter with selected entity slugs to create endpoint filters.`
47
+ );
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: lines.join("\n")
53
+ }
54
+ ]
55
+ };
56
+ }
57
+ );
58
+ server2.tool(
59
+ "stackwright_pro_generate_filter",
60
+ "Generate endpoint filter configuration from selected entities. Creates include/exclude patterns for stackwright.yml OpenAPI integration. Use this after stackwright_pro_list_entities to select which API endpoints the application needs. Only selected endpoints will generate client code, reducing bundle size and improving security.",
61
+ {
62
+ selectedEntities: z.array(z.string()).describe('Entity slugs to include (e.g., ["equipment", "supplies"])'),
63
+ excludePatterns: z.array(z.string()).optional().describe('Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'),
64
+ projectRoot: z.string().optional().describe("Project root directory")
65
+ },
66
+ async ({ selectedEntities, excludePatterns, projectRoot }) => {
67
+ const result = generateFilter({
68
+ selectedEntities,
69
+ ...excludePatterns !== void 0 && { excludePatterns },
70
+ projectRoot: projectRoot ?? process.cwd()
71
+ });
72
+ if (!result.success) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: "text",
77
+ text: `Error generating filter: ${result.warnings?.join(", ")}`
78
+ }
79
+ ],
80
+ isError: true
81
+ };
82
+ }
83
+ const lines = ["\u{1F510} Generated Endpoint Filter:\n"];
84
+ lines.push("```yaml");
85
+ lines.push("integrations:");
86
+ lines.push(" - type: openapi");
87
+ lines.push(" endpoints:");
88
+ if (result.filter.include && result.filter.include.length > 0) {
89
+ lines.push(" include:");
90
+ for (const path3 of result.filter.include) {
91
+ lines.push(` - ${path3}`);
92
+ }
93
+ }
94
+ if (result.filter.exclude && result.filter.exclude.length > 0) {
95
+ lines.push(" exclude:");
96
+ for (const pattern of result.filter.exclude) {
97
+ lines.push(` - ${pattern}`);
98
+ }
99
+ }
100
+ lines.push("```");
101
+ if (result.warnings && result.warnings.length > 0) {
102
+ lines.push("\n\u26A0\uFE0F Warnings:");
103
+ for (const warning of result.warnings) {
104
+ lines.push(` ${warning}`);
105
+ }
106
+ }
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: lines.join("\n")
112
+ }
113
+ ]
114
+ };
115
+ }
116
+ );
117
+ }
118
+
119
+ // src/tools/security.ts
120
+ import { z as z2 } from "zod";
121
+ import { createHash } from "crypto";
122
+ import fs from "fs";
123
+ import path from "path";
124
+ function registerSecurityTools(server2) {
125
+ server2.tool(
126
+ "stackwright_pro_validate_spec",
127
+ "Validate an OpenAPI spec against the enterprise approved-specs configuration. Checks if the spec URL is on the allowlist and verifies SHA-256 hash integrity. Use this in enterprise environments where only pre-approved API specs are allowed. Fails build if spec is not approved or has been modified.",
128
+ {
129
+ specPath: z2.string().describe("URL or file path to the OpenAPI spec to validate"),
130
+ configPath: z2.string().optional().describe("Path to stackwright.yml with prebuild.security config")
131
+ },
132
+ async ({ specPath, configPath }) => {
133
+ let securityEnabled = false;
134
+ const allowlist = [];
135
+ const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
136
+ if (fs.existsSync(configFile)) {
137
+ try {
138
+ const content = fs.readFileSync(configFile, "utf8");
139
+ const securityMatch = content.match(
140
+ /prebuild:\s*\n\s*security:\s*\n\s*enabled:\s*(true|false)/
141
+ );
142
+ if (securityMatch && securityMatch[1] === "true") {
143
+ securityEnabled = true;
144
+ const specMatches = content.matchAll(
145
+ /- name:\s*(.+?)\n\s+url:\s*(.+?)\n\s+sha256:\s*(.+?)(?:\n|$)/g
146
+ );
147
+ for (const match of specMatches) {
148
+ allowlist.push({
149
+ name: match[1].trim(),
150
+ url: match[2].trim(),
151
+ sha256: match[3].trim()
152
+ });
153
+ }
154
+ }
155
+ } catch {
156
+ }
157
+ }
158
+ if (!securityEnabled) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: `\u2705 Spec validation skipped (security not enabled)
164
+
165
+ To enable approved-specs validation, add to stackwright.yml:
166
+
167
+ prebuild:
168
+ security:
169
+ enabled: true
170
+ allowlist:
171
+ - name: My API
172
+ url: ${specPath}
173
+ sha256: <sha256-of-spec>`
174
+ }
175
+ ]
176
+ };
177
+ }
178
+ const matchingSpec = allowlist.find((s) => s.url === specPath);
179
+ if (!matchingSpec) {
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: `\u274C Spec rejected!
185
+
186
+ Error Code: SPEC_NOT_IN_ALLOWLIST
187
+
188
+ URL: ${specPath}
189
+
190
+ This spec is not on the approved-specs allowlist. Add it using stackwright_pro_add_approved_spec before proceeding.`
191
+ }
192
+ ],
193
+ isError: true
194
+ };
195
+ }
196
+ if (!specPath.startsWith("http://") && !specPath.startsWith("https://")) {
197
+ if (fs.existsSync(specPath)) {
198
+ const specContent = fs.readFileSync(specPath, "utf8");
199
+ const sha256 = createHash("sha256").update(specContent).digest("hex");
200
+ if (sha256 !== matchingSpec.sha256) {
201
+ return {
202
+ content: [
203
+ {
204
+ type: "text",
205
+ text: `\u274C Spec hash mismatch!
206
+
207
+ Error Code: SPEC_HASH_MISMATCH
208
+
209
+ Expected: ${matchingSpec.sha256.substring(0, 16)}...
210
+ Actual: ${sha256.substring(0, 16)}...
211
+
212
+ The spec has been modified since it was approved. Re-approve the spec to continue.`
213
+ }
214
+ ],
215
+ isError: true
216
+ };
217
+ }
218
+ }
219
+ }
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `\u2705 Spec approved!
225
+
226
+ Name: ${matchingSpec.name}
227
+ URL: ${specPath}
228
+ Status: Valid (${allowlist.length} specs on allowlist)`
229
+ }
230
+ ]
231
+ };
232
+ }
233
+ );
234
+ server2.tool(
235
+ "stackwright_pro_add_approved_spec",
236
+ "Add an OpenAPI spec to the approved-specs allowlist in stackwright.yml. Computes the SHA-256 hash of the spec and adds it to the security configuration. Use this when onboarding a new API in enterprise environments.",
237
+ {
238
+ name: z2.string().describe("Human-readable name for the spec"),
239
+ url: z2.string().describe("URL or file path to the OpenAPI spec"),
240
+ configPath: z2.string().optional().describe("Path to stackwright.yml")
241
+ },
242
+ async ({ name, url, configPath }) => {
243
+ const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
244
+ let sha256 = "<computed-at-build>";
245
+ try {
246
+ if (url.startsWith("http://") || url.startsWith("https://")) {
247
+ sha256 = "<computed-at-build>";
248
+ } else if (fs.existsSync(url)) {
249
+ const specContent = fs.readFileSync(url, "utf8");
250
+ sha256 = createHash("sha256").update(specContent).digest("hex");
251
+ } else {
252
+ return {
253
+ content: [
254
+ {
255
+ type: "text",
256
+ text: `\u274C Spec file not found: ${url}`
257
+ }
258
+ ],
259
+ isError: true
260
+ };
261
+ }
262
+ } catch (e) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: `\u274C Failed to read spec: ${e}`
268
+ }
269
+ ],
270
+ isError: true
271
+ };
272
+ }
273
+ const yamlSnippet = ` - name: ${name}
274
+ url: ${url}
275
+ sha256: ${sha256}`;
276
+ return {
277
+ content: [
278
+ {
279
+ type: "text",
280
+ text: `\u{1F4DD} Add this to stackwright.yml under prebuild.security.allowlist:
281
+
282
+ ${yamlSnippet}
283
+
284
+ SHA-256 computed: ${sha256}`
285
+ }
286
+ ]
287
+ };
288
+ }
289
+ );
290
+ server2.tool(
291
+ "stackwright_pro_list_approved_specs",
292
+ "List all specs currently on the approved-specs allowlist. Shows spec names, URLs, and hash prefixes. Use this to audit what APIs are approved in the project.",
293
+ {
294
+ configPath: z2.string().optional().describe("Path to stackwright.yml")
295
+ },
296
+ async ({ configPath }) => {
297
+ const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
298
+ if (!fs.existsSync(configFile)) {
299
+ return {
300
+ content: [
301
+ {
302
+ type: "text",
303
+ text: "\u274C stackwright.yml not found in project root."
304
+ }
305
+ ],
306
+ isError: true
307
+ };
308
+ }
309
+ const content = fs.readFileSync(configFile, "utf8");
310
+ const securityEnabled = content.includes("prebuild:") && content.includes("security:") && content.includes("enabled: true");
311
+ if (!securityEnabled) {
312
+ return {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: "\u2139\uFE0F Approved-specs validation is not enabled.\n\nAdd to stackwright.yml to enable:\n\nprebuild:\n security:\n enabled: true\n allowlist: []"
317
+ }
318
+ ]
319
+ };
320
+ }
321
+ const specs = [];
322
+ const specMatches = content.matchAll(
323
+ /- name:\s*(.+?)\n\s+url:\s*(.+?)\n\s+sha256:\s*(.+?)(?:\n|$)/g
324
+ );
325
+ for (const match of specMatches) {
326
+ specs.push({
327
+ name: match[1].trim(),
328
+ url: match[2].trim(),
329
+ sha256: match[3].trim().substring(0, 16) + "..."
330
+ });
331
+ }
332
+ if (specs.length === 0) {
333
+ return {
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: "\u2139\uFE0F Security enabled but no specs on allowlist.\n\nAdd specs using stackwright_pro_add_approved_spec."
338
+ }
339
+ ]
340
+ };
341
+ }
342
+ const lines = [`\u{1F510} Approved Specs (${specs.length}):
343
+ `];
344
+ for (const spec of specs) {
345
+ lines.push(`\u{1F4E6} ${spec.name}`);
346
+ lines.push(` URL: ${spec.url}`);
347
+ lines.push(` Hash: ${spec.sha256}`);
348
+ lines.push("");
349
+ }
350
+ return {
351
+ content: [
352
+ {
353
+ type: "text",
354
+ text: lines.join("\n")
355
+ }
356
+ ]
357
+ };
358
+ }
359
+ );
360
+ }
361
+
362
+ // src/tools/isr.ts
363
+ import { z as z3 } from "zod";
364
+ function registerIsrTools(server2) {
365
+ server2.tool(
366
+ "stackwright_pro_configure_isr",
367
+ "Configure Incremental Static Regeneration (ISR) for an API-backed collection. ISR allows API data to be cached and refreshed on a schedule, providing real-time data with the performance of static generation. Use this after stackwright_pro_generate_filter to set revalidation intervals.",
368
+ {
369
+ collection: z3.string().describe('Collection name (e.g., "equipment", "supplies")'),
370
+ revalidateSeconds: z3.number().optional().describe("Revalidation interval in seconds (default: 60)"),
371
+ fallback: z3.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
372
+ configPath: z3.string().optional().describe("Path to stackwright.yml")
373
+ },
374
+ async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
375
+ const revalidate = revalidateSeconds;
376
+ const yamlSnippet = `# Add to stackwright.yml under integrations:
377
+ - type: openapi
378
+ name: ${collection}
379
+ # ... other config
380
+ isr:
381
+ revalidate: ${revalidate}
382
+ fallback: ${fallback}`;
383
+ let description = "";
384
+ if (revalidate < 60) {
385
+ description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
386
+ } else if (revalidate < 300) {
387
+ description = "\u{1F4CA} Fresh data (revalidate every " + (revalidate / 60).toFixed(0) + " min)";
388
+ } else if (revalidate < 3600) {
389
+ description = "\u{1F4BE} Cached data (revalidate every " + (revalidate / 60).toFixed(0) + " min)";
390
+ } else {
391
+ description = "\u{1F5C4}\uFE0F Static data (revalidate every " + (revalidate / 3600).toFixed(0) + " hour)";
392
+ }
393
+ return {
394
+ content: [
395
+ {
396
+ type: "text",
397
+ text: `\u2699\uFE0F ISR Configuration for "${collection}":
398
+
399
+ ${description}
400
+
401
+ Fallback: ${fallback === "blocking" ? "\u23F3 Show loading until cached" : fallback === "true" ? "\u{1F4C4} Generate on demand" : "\u274C 404 for new pages"}
402
+
403
+ \`\`\`yaml
404
+ ${yamlSnippet}
405
+ \`\`\``
406
+ }
407
+ ]
408
+ };
409
+ }
410
+ );
411
+ server2.tool(
412
+ "stackwright_pro_configure_isr_batch",
413
+ "Configure ISR for multiple collections at once. More efficient than calling stackwright_pro_configure_isr multiple times. Provide different revalidation intervals based on data freshness requirements.",
414
+ {
415
+ collections: z3.array(
416
+ z3.object({
417
+ name: z3.string().describe("Collection name"),
418
+ revalidateSeconds: z3.number().optional().describe("Revalidation interval")
419
+ })
420
+ ).describe("Array of collection configurations"),
421
+ defaultFallback: z3.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
422
+ configPath: z3.string().optional().describe("Path to stackwright.yml")
423
+ },
424
+ async ({ collections, defaultFallback = "blocking", configPath }) => {
425
+ const lines = [`\u2699\uFE0F Batch ISR Configuration:
426
+ `];
427
+ for (const col of collections) {
428
+ const rev = col.revalidateSeconds || 60;
429
+ let freshness = "\u{1F4BE}";
430
+ if (rev < 60) freshness = "\u26A1";
431
+ else if (rev < 300) freshness = "\u{1F4CA}";
432
+ else if (rev >= 3600) freshness = "\u{1F5C4}\uFE0F";
433
+ lines.push(`${freshness} ${col.name}: revalidate=${rev}s`);
434
+ }
435
+ lines.push(`
436
+ Fallback: ${defaultFallback}`);
437
+ return {
438
+ content: [
439
+ {
440
+ type: "text",
441
+ text: lines.join("\n")
442
+ }
443
+ ]
444
+ };
445
+ }
446
+ );
447
+ }
448
+
449
+ // src/tools/dashboard.ts
450
+ import { z as z4 } from "zod";
451
+ import { listEntities as listEntities2 } from "@stackwright-pro/cli-data-explorer";
452
+ function registerDashboardTools(server2) {
453
+ server2.tool(
454
+ "stackwright_pro_generate_dashboard",
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.",
456
+ {
457
+ entities: z4.array(z4.string()).describe("Entity slugs to include in dashboard"),
458
+ layout: z4.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
459
+ pageTitle: z4.string().optional().describe("Page title"),
460
+ specPath: z4.string().optional().describe("Path to OpenAPI spec for entity details")
461
+ },
462
+ async ({ entities, layout = "mixed", pageTitle, specPath }) => {
463
+ let entityDetails = [];
464
+ if (specPath) {
465
+ const result = listEntities2({ specPath });
466
+ if (result.success) {
467
+ entityDetails = result.entities.filter((e) => entities.includes(e.slug));
468
+ }
469
+ }
470
+ const title = pageTitle || entities.map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" & ");
471
+ const yamlLines = [
472
+ "# Dashboard page configuration",
473
+ "# Generated by Stackwright Pro MCP",
474
+ "",
475
+ "content:",
476
+ " meta:",
477
+ ` title: "${title} | Dashboard"`,
478
+ ` description: "Live data from API"`,
479
+ "",
480
+ " content_items:"
481
+ ];
482
+ if (layout === "grid" || layout === "mixed") {
483
+ for (const slug of entities) {
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`);
495
+ yamlLines.push("");
496
+ }
497
+ }
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"];
504
+ yamlLines.push(
505
+ ` columns: ${entityDetails[0]?.fields?.slice(0, 4).map((f) => f.name).join(", ") || defaultCols.join(", ")}`
506
+ );
507
+ yamlLines.push("");
508
+ if (layout === "table") {
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`);
522
+ }
523
+ const yaml = yamlLines.join("\n");
524
+ return {
525
+ content: [
526
+ {
527
+ type: "text",
528
+ text: `\u{1F4CA} Generated Dashboard for: ${entities.join(", ")}
529
+
530
+ Layout: ${layout}
531
+
532
+ \`\`\`yaml
533
+ ${yaml}
534
+ \`\`\`
535
+
536
+ \u{1F4A1} Add this to pages/dashboard/content.yml and validate with stackwright_validate_pages.`
537
+ }
538
+ ]
539
+ };
540
+ }
541
+ );
542
+ server2.tool(
543
+ "stackwright_pro_generate_detail_page",
544
+ "Generate a detail view page for a single API entity. Creates YAML content for displaying all fields of an individual record. Use this to create detail pages that complement collection listing pages.",
545
+ {
546
+ entity: z4.string().describe('Entity slug (e.g., "equipment")'),
547
+ slugField: z4.string().optional().describe('Field to use as URL slug (default: "id")'),
548
+ specPath: z4.string().optional().describe("Path to OpenAPI spec for field details")
549
+ },
550
+ async ({ entity, slugField = "id", specPath }) => {
551
+ let fields = [];
552
+ if (specPath) {
553
+ const result = listEntities2({ specPath });
554
+ if (result.success) {
555
+ const ent = result.entities.find((e) => e.slug === entity);
556
+ if (ent) {
557
+ fields = ent.fields;
558
+ }
559
+ }
560
+ }
561
+ const entityName = entity.charAt(0).toUpperCase() + entity.slice(1);
562
+ const yamlLines = [
563
+ `# Detail page for ${entityName}`,
564
+ "# Generated by Stackwright Pro MCP",
565
+ "",
566
+ "content:",
567
+ " meta:",
568
+ ` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
569
+ "",
570
+ " content_items:",
571
+ " - main:",
572
+ ' label: "detail-header"',
573
+ " heading:",
574
+ ` text: "{{ ${entity}.${slugField} }}"`,
575
+ ' textSize: "h1"',
576
+ " textBlocks:",
577
+ ` - text: "Details for this ${entity}"`,
578
+ ' textSize: "body1"',
579
+ ' background: "primary"',
580
+ ' color: "text"',
581
+ "",
582
+ " - grid:",
583
+ ' label: "detail-fields"',
584
+ " columns: 2",
585
+ " items:"
586
+ ];
587
+ const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
588
+ { name: slugField, type: "string" },
589
+ { name: "created_at", type: "datetime" },
590
+ { name: "updated_at", type: "datetime" }
591
+ ];
592
+ for (const field of displayFields) {
593
+ const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
594
+ yamlLines.push(` - card:`);
595
+ yamlLines.push(` label: "${field.name}-field"`);
596
+ yamlLines.push(` heading:`);
597
+ yamlLines.push(` text: "${label}"`);
598
+ yamlLines.push(` textSize: "h4"`);
599
+ yamlLines.push(` content:`);
600
+ yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
601
+ yamlLines.push(` textSize: "body1"`);
602
+ }
603
+ yamlLines.push(' background: "background"');
604
+ const yaml = yamlLines.join("\n");
605
+ return {
606
+ content: [
607
+ {
608
+ type: "text",
609
+ text: `\u{1F4C4} Generated Detail Page for: ${entity}
610
+
611
+ \`\`\`yaml
612
+ ${yaml}
613
+ \`\`\`
614
+
615
+ \u{1F4A1} Add this to pages/${entity}/[${slugField}]/content.yml and validate.`
616
+ }
617
+ ]
618
+ };
619
+ }
620
+ );
621
+ }
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.0.0-beta-20260417191149",
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
+ publishConfig: {
1219
+ access: "public"
1220
+ }
1221
+ };
1222
+
1223
+ // src/server.ts
1224
+ var server = new McpServer({
1225
+ name: "stackwright-pro",
1226
+ version: package_default.version
1227
+ });
1228
+ registerDataExplorerTools(server);
1229
+ registerSecurityTools(server);
1230
+ registerIsrTools(server);
1231
+ registerDashboardTools(server);
1232
+ registerClarificationTools(server);
1233
+ registerPackageTools(server);
1234
+ async function main() {
1235
+ const transport = new StdioServerTransport();
1236
+ await server.connect(transport);
1237
+ console.error("Stackwright Pro MCP server running on stdio");
1238
+ }
1239
+ main().catch((err) => {
1240
+ console.error("Fatal:", err);
1241
+ process.exit(1);
1242
+ });
1243
+ //# sourceMappingURL=server.mjs.map