@stackwright-pro/mcp 0.1.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.d.mts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +670 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +653 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +35 -0
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/server.ts
|
|
26
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
27
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
28
|
+
|
|
29
|
+
// src/tools/data-explorer.ts
|
|
30
|
+
var import_zod = require("zod");
|
|
31
|
+
var import_cli_data_explorer = require("@stackwright-pro/cli-data-explorer");
|
|
32
|
+
function registerDataExplorerTools(server2) {
|
|
33
|
+
server2.tool(
|
|
34
|
+
"stackwright_pro_list_entities",
|
|
35
|
+
"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.",
|
|
36
|
+
{
|
|
37
|
+
specPath: import_zod.z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
|
|
38
|
+
projectRoot: import_zod.z.string().optional().describe("Project root directory (auto-detected if omitted)")
|
|
39
|
+
},
|
|
40
|
+
async ({ specPath, projectRoot }) => {
|
|
41
|
+
const result = (0, import_cli_data_explorer.listEntities)({
|
|
42
|
+
specPath,
|
|
43
|
+
projectRoot
|
|
44
|
+
});
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Error discovering entities: ${(result.errors ?? []).join(", ")}`
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
isError: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const lines = [`\u{1F4E6} Found ${result.entities.length} API Entities:
|
|
57
|
+
`];
|
|
58
|
+
for (const entity of result.entities) {
|
|
59
|
+
lines.push(`\u{1F4E6} ${entity.name} (${entity.slug})`);
|
|
60
|
+
lines.push(` Endpoint: ${entity.endpoint}`);
|
|
61
|
+
lines.push(` Fields: ${entity.fieldCount}`);
|
|
62
|
+
if (entity.fields.length > 0) {
|
|
63
|
+
const fieldTypes = entity.fields.slice(0, 3).map((f) => `${f.name}:${f.type}`).join(", ");
|
|
64
|
+
lines.push(` Types: ${fieldTypes}${entity.fields.length > 3 ? "..." : ""}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
lines.push(
|
|
69
|
+
`
|
|
70
|
+
\u{1F4A1} Use stackwright_pro_generate_filter with selected entity slugs to create endpoint filters.`
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: lines.join("\n")
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
server2.tool(
|
|
83
|
+
"stackwright_pro_generate_filter",
|
|
84
|
+
"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.",
|
|
85
|
+
{
|
|
86
|
+
selectedEntities: import_zod.z.array(import_zod.z.string()).describe('Entity slugs to include (e.g., ["equipment", "supplies"])'),
|
|
87
|
+
excludePatterns: import_zod.z.array(import_zod.z.string()).optional().describe('Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'),
|
|
88
|
+
projectRoot: import_zod.z.string().optional().describe("Project root directory")
|
|
89
|
+
},
|
|
90
|
+
async ({ selectedEntities, excludePatterns, projectRoot }) => {
|
|
91
|
+
const result = (0, import_cli_data_explorer.generateFilter)({
|
|
92
|
+
selectedEntities,
|
|
93
|
+
excludePatterns,
|
|
94
|
+
projectRoot: projectRoot || process.cwd()
|
|
95
|
+
});
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Error generating filter: ${result.warnings?.join(", ")}`
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
isError: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const lines = ["\u{1F510} Generated Endpoint Filter:\n"];
|
|
108
|
+
lines.push("```yaml");
|
|
109
|
+
lines.push("integrations:");
|
|
110
|
+
lines.push(" - type: openapi");
|
|
111
|
+
lines.push(" endpoints:");
|
|
112
|
+
if (result.filter.include && result.filter.include.length > 0) {
|
|
113
|
+
lines.push(" include:");
|
|
114
|
+
for (const path2 of result.filter.include) {
|
|
115
|
+
lines.push(` - ${path2}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (result.filter.exclude && result.filter.exclude.length > 0) {
|
|
119
|
+
lines.push(" exclude:");
|
|
120
|
+
for (const pattern of result.filter.exclude) {
|
|
121
|
+
lines.push(` - ${pattern}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push("```");
|
|
125
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
126
|
+
lines.push("\n\u26A0\uFE0F Warnings:");
|
|
127
|
+
for (const warning of result.warnings) {
|
|
128
|
+
lines.push(` ${warning}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: lines.join("\n")
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/tools/security.ts
|
|
144
|
+
var import_zod2 = require("zod");
|
|
145
|
+
var import_fs = __toESM(require("fs"));
|
|
146
|
+
var import_path = __toESM(require("path"));
|
|
147
|
+
function registerSecurityTools(server2) {
|
|
148
|
+
server2.tool(
|
|
149
|
+
"stackwright_pro_validate_spec",
|
|
150
|
+
"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.",
|
|
151
|
+
{
|
|
152
|
+
specPath: import_zod2.z.string().describe("URL or file path to the OpenAPI spec to validate"),
|
|
153
|
+
configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml with prebuild.security config")
|
|
154
|
+
},
|
|
155
|
+
async ({ specPath, configPath }) => {
|
|
156
|
+
let securityEnabled = false;
|
|
157
|
+
let allowlist = [];
|
|
158
|
+
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
159
|
+
if (import_fs.default.existsSync(configFile)) {
|
|
160
|
+
try {
|
|
161
|
+
const content = import_fs.default.readFileSync(configFile, "utf8");
|
|
162
|
+
const securityMatch = content.match(
|
|
163
|
+
/prebuild:\s*\n\s*security:\s*\n\s*enabled:\s*(true|false)/
|
|
164
|
+
);
|
|
165
|
+
if (securityMatch && securityMatch[1] === "true") {
|
|
166
|
+
securityEnabled = true;
|
|
167
|
+
const specMatches = content.matchAll(
|
|
168
|
+
/- name:\s*(.+?)\n\s+url:\s*(.+?)\n\s+sha256:\s*(.+?)(?:\n|$)/g
|
|
169
|
+
);
|
|
170
|
+
for (const match of specMatches) {
|
|
171
|
+
allowlist.push({
|
|
172
|
+
name: match[1].trim(),
|
|
173
|
+
url: match[2].trim(),
|
|
174
|
+
sha256: match[3].trim()
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!securityEnabled) {
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: `\u2705 Spec validation skipped (security not enabled)
|
|
187
|
+
|
|
188
|
+
To enable approved-specs validation, add to stackwright.yml:
|
|
189
|
+
|
|
190
|
+
prebuild:
|
|
191
|
+
security:
|
|
192
|
+
enabled: true
|
|
193
|
+
allowlist:
|
|
194
|
+
- name: My API
|
|
195
|
+
url: ${specPath}
|
|
196
|
+
sha256: <sha256-of-spec>`
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const matchingSpec = allowlist.find((s) => s.url === specPath);
|
|
202
|
+
if (!matchingSpec) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `\u274C Spec rejected!
|
|
208
|
+
|
|
209
|
+
Error Code: SPEC_NOT_IN_ALLOWLIST
|
|
210
|
+
|
|
211
|
+
URL: ${specPath}
|
|
212
|
+
|
|
213
|
+
This spec is not on the approved-specs allowlist. Add it using stackwright_pro_add_approved_spec before proceeding.`
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
isError: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (!specPath.startsWith("http://") && !specPath.startsWith("https://")) {
|
|
220
|
+
if (import_fs.default.existsSync(specPath)) {
|
|
221
|
+
const specContent = import_fs.default.readFileSync(specPath, "utf8");
|
|
222
|
+
const crypto = require("crypto");
|
|
223
|
+
const sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
|
|
224
|
+
if (sha256 !== matchingSpec.sha256) {
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: `\u274C Spec hash mismatch!
|
|
230
|
+
|
|
231
|
+
Error Code: SPEC_HASH_MISMATCH
|
|
232
|
+
|
|
233
|
+
Expected: ${matchingSpec.sha256.substring(0, 16)}...
|
|
234
|
+
Actual: ${sha256.substring(0, 16)}...
|
|
235
|
+
|
|
236
|
+
The spec has been modified since it was approved. Re-approve the spec to continue.`
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
isError: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: `\u2705 Spec approved!
|
|
249
|
+
|
|
250
|
+
Name: ${matchingSpec.name}
|
|
251
|
+
URL: ${specPath}
|
|
252
|
+
Status: Valid (${allowlist.length} specs on allowlist)`
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
server2.tool(
|
|
259
|
+
"stackwright_pro_add_approved_spec",
|
|
260
|
+
"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.",
|
|
261
|
+
{
|
|
262
|
+
name: import_zod2.z.string().describe("Human-readable name for the spec"),
|
|
263
|
+
url: import_zod2.z.string().describe("URL or file path to the OpenAPI spec"),
|
|
264
|
+
configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml")
|
|
265
|
+
},
|
|
266
|
+
async ({ name, url, configPath }) => {
|
|
267
|
+
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
268
|
+
let sha256 = "<computed-at-build>";
|
|
269
|
+
try {
|
|
270
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
271
|
+
sha256 = "<computed-at-build>";
|
|
272
|
+
} else if (import_fs.default.existsSync(url)) {
|
|
273
|
+
const specContent = import_fs.default.readFileSync(url, "utf8");
|
|
274
|
+
const crypto = require("crypto");
|
|
275
|
+
sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
|
|
276
|
+
} else {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `\u274C Spec file not found: ${url}`
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
isError: true
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: `\u274C Failed to read spec: ${e}`
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
isError: true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const yamlSnippet = ` - name: ${name}
|
|
299
|
+
url: ${url}
|
|
300
|
+
sha256: ${sha256}`;
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: `\u{1F4DD} Add this to stackwright.yml under prebuild.security.allowlist:
|
|
306
|
+
|
|
307
|
+
${yamlSnippet}
|
|
308
|
+
|
|
309
|
+
SHA-256 computed: ${sha256}`
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
server2.tool(
|
|
316
|
+
"stackwright_pro_list_approved_specs",
|
|
317
|
+
"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.",
|
|
318
|
+
{
|
|
319
|
+
configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml")
|
|
320
|
+
},
|
|
321
|
+
async ({ configPath }) => {
|
|
322
|
+
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
323
|
+
if (!import_fs.default.existsSync(configFile)) {
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: "\u274C stackwright.yml not found in project root."
|
|
329
|
+
}
|
|
330
|
+
],
|
|
331
|
+
isError: true
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const content = import_fs.default.readFileSync(configFile, "utf8");
|
|
335
|
+
const securityEnabled = content.includes("prebuild:") && content.includes("security:") && content.includes("enabled: true");
|
|
336
|
+
if (!securityEnabled) {
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
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: []"
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const specs = [];
|
|
347
|
+
const specMatches = content.matchAll(
|
|
348
|
+
/- name:\s*(.+?)\n\s+url:\s*(.+?)\n\s+sha256:\s*(.+?)(?:\n|$)/g
|
|
349
|
+
);
|
|
350
|
+
for (const match of specMatches) {
|
|
351
|
+
specs.push({
|
|
352
|
+
name: match[1].trim(),
|
|
353
|
+
url: match[2].trim(),
|
|
354
|
+
sha256: match[3].trim().substring(0, 16) + "..."
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (specs.length === 0) {
|
|
358
|
+
return {
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: "\u2139\uFE0F Security enabled but no specs on allowlist.\n\nAdd specs using stackwright_pro_add_approved_spec."
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const lines = [`\u{1F510} Approved Specs (${specs.length}):
|
|
368
|
+
`];
|
|
369
|
+
for (const spec of specs) {
|
|
370
|
+
lines.push(`\u{1F4E6} ${spec.name}`);
|
|
371
|
+
lines.push(` URL: ${spec.url}`);
|
|
372
|
+
lines.push(` Hash: ${spec.sha256}`);
|
|
373
|
+
lines.push("");
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: lines.join("\n")
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/tools/isr.ts
|
|
388
|
+
var import_zod3 = require("zod");
|
|
389
|
+
function registerIsrTools(server2) {
|
|
390
|
+
server2.tool(
|
|
391
|
+
"stackwright_pro_configure_isr",
|
|
392
|
+
"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.",
|
|
393
|
+
{
|
|
394
|
+
collection: import_zod3.z.string().describe('Collection name (e.g., "equipment", "supplies")'),
|
|
395
|
+
revalidateSeconds: import_zod3.z.number().optional().describe("Revalidation interval in seconds (default: 60)"),
|
|
396
|
+
fallback: import_zod3.z.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
|
|
397
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
398
|
+
},
|
|
399
|
+
async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
|
|
400
|
+
const revalidate = revalidateSeconds;
|
|
401
|
+
const yamlSnippet = `# Add to stackwright.yml under integrations:
|
|
402
|
+
- type: openapi
|
|
403
|
+
name: ${collection}
|
|
404
|
+
# ... other config
|
|
405
|
+
isr:
|
|
406
|
+
revalidate: ${revalidate}
|
|
407
|
+
fallback: ${fallback}`;
|
|
408
|
+
let description = "";
|
|
409
|
+
if (revalidate < 60) {
|
|
410
|
+
description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
|
|
411
|
+
} else if (revalidate < 300) {
|
|
412
|
+
description = "\u{1F4CA} Fresh data (revalidate every " + (revalidate / 60).toFixed(0) + " min)";
|
|
413
|
+
} else if (revalidate < 3600) {
|
|
414
|
+
description = "\u{1F4BE} Cached data (revalidate every " + (revalidate / 60).toFixed(0) + " min)";
|
|
415
|
+
} else {
|
|
416
|
+
description = "\u{1F5C4}\uFE0F Static data (revalidate every " + (revalidate / 3600).toFixed(0) + " hour)";
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: `\u2699\uFE0F ISR Configuration for "${collection}":
|
|
423
|
+
|
|
424
|
+
${description}
|
|
425
|
+
|
|
426
|
+
Fallback: ${fallback === "blocking" ? "\u23F3 Show loading until cached" : fallback === "true" ? "\u{1F4C4} Generate on demand" : "\u274C 404 for new pages"}
|
|
427
|
+
|
|
428
|
+
\`\`\`yaml
|
|
429
|
+
${yamlSnippet}
|
|
430
|
+
\`\`\``
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
server2.tool(
|
|
437
|
+
"stackwright_pro_configure_isr_batch",
|
|
438
|
+
"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.",
|
|
439
|
+
{
|
|
440
|
+
collections: import_zod3.z.array(
|
|
441
|
+
import_zod3.z.object({
|
|
442
|
+
name: import_zod3.z.string().describe("Collection name"),
|
|
443
|
+
revalidateSeconds: import_zod3.z.number().optional().describe("Revalidation interval")
|
|
444
|
+
})
|
|
445
|
+
).describe("Array of collection configurations"),
|
|
446
|
+
defaultFallback: import_zod3.z.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
|
|
447
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
448
|
+
},
|
|
449
|
+
async ({ collections, defaultFallback = "blocking", configPath }) => {
|
|
450
|
+
const lines = [`\u2699\uFE0F Batch ISR Configuration:
|
|
451
|
+
`];
|
|
452
|
+
for (const col of collections) {
|
|
453
|
+
const rev = col.revalidateSeconds || 60;
|
|
454
|
+
let freshness = "\u{1F4BE}";
|
|
455
|
+
if (rev < 60) freshness = "\u26A1";
|
|
456
|
+
else if (rev < 300) freshness = "\u{1F4CA}";
|
|
457
|
+
else if (rev >= 3600) freshness = "\u{1F5C4}\uFE0F";
|
|
458
|
+
lines.push(`${freshness} ${col.name}: revalidate=${rev}s`);
|
|
459
|
+
}
|
|
460
|
+
lines.push(`
|
|
461
|
+
Fallback: ${defaultFallback}`);
|
|
462
|
+
return {
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: "text",
|
|
466
|
+
text: lines.join("\n")
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/tools/dashboard.ts
|
|
475
|
+
var import_zod4 = require("zod");
|
|
476
|
+
var import_cli_data_explorer2 = require("@stackwright-pro/cli-data-explorer");
|
|
477
|
+
function registerDashboardTools(server2) {
|
|
478
|
+
server2.tool(
|
|
479
|
+
"stackwright_pro_generate_dashboard",
|
|
480
|
+
"Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with collection_listing, data_table, or stats_grid content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
|
|
481
|
+
{
|
|
482
|
+
entities: import_zod4.z.array(import_zod4.z.string()).describe("Entity slugs to include in dashboard"),
|
|
483
|
+
layout: import_zod4.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
|
|
484
|
+
pageTitle: import_zod4.z.string().optional().describe("Page title"),
|
|
485
|
+
specPath: import_zod4.z.string().optional().describe("Path to OpenAPI spec for entity details")
|
|
486
|
+
},
|
|
487
|
+
async ({ entities, layout = "mixed", pageTitle, specPath }) => {
|
|
488
|
+
let entityDetails = [];
|
|
489
|
+
if (specPath) {
|
|
490
|
+
const result = (0, import_cli_data_explorer2.listEntities)({ specPath });
|
|
491
|
+
if (result.success) {
|
|
492
|
+
entityDetails = result.entities.filter((e) => entities.includes(e.slug));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const title = pageTitle || entities.map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" & ");
|
|
496
|
+
const yamlLines = [
|
|
497
|
+
"# Dashboard page configuration",
|
|
498
|
+
"# Generated by Stackwright Pro MCP",
|
|
499
|
+
"",
|
|
500
|
+
"content:",
|
|
501
|
+
" meta:",
|
|
502
|
+
` title: "${title} | Dashboard"`,
|
|
503
|
+
` description: "Live data from API"`,
|
|
504
|
+
"",
|
|
505
|
+
" content_items:"
|
|
506
|
+
];
|
|
507
|
+
if (layout === "grid" || layout === "mixed") {
|
|
508
|
+
for (const slug of entities) {
|
|
509
|
+
yamlLines.push(` - stats_grid:`);
|
|
510
|
+
yamlLines.push(` label: "${slug}-stats"`);
|
|
511
|
+
yamlLines.push(` heading:`);
|
|
512
|
+
yamlLines.push(
|
|
513
|
+
` text: "${slug.charAt(0).toUpperCase() + slug.slice(1)} Overview"`
|
|
514
|
+
);
|
|
515
|
+
yamlLines.push(` textSize: "h2"`);
|
|
516
|
+
yamlLines.push(` stats:`);
|
|
517
|
+
yamlLines.push(` - label: "Total"`);
|
|
518
|
+
yamlLines.push(` value: "{{ ${slug}.count }}"`);
|
|
519
|
+
yamlLines.push(` icon: "Database"`);
|
|
520
|
+
yamlLines.push(` source: "${slug}"`);
|
|
521
|
+
yamlLines.push(` background: "surface"`);
|
|
522
|
+
yamlLines.push("");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
yamlLines.push(` - collection_listing:`);
|
|
526
|
+
yamlLines.push(` label: "${entities[0]}-listing"`);
|
|
527
|
+
yamlLines.push(` heading:`);
|
|
528
|
+
yamlLines.push(
|
|
529
|
+
` text: "${entities.map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" & ")}"`
|
|
530
|
+
);
|
|
531
|
+
yamlLines.push(` textSize: "h2"`);
|
|
532
|
+
yamlLines.push(` collection: "${entities[0]}"`);
|
|
533
|
+
yamlLines.push(` showFilters: true`);
|
|
534
|
+
yamlLines.push(` showSearch: true`);
|
|
535
|
+
const defaultCols = ['"id"', '"name"', '"status"', '"created_at"'];
|
|
536
|
+
yamlLines.push(
|
|
537
|
+
` columns: ${entityDetails[0]?.fields?.slice(0, 4).map((f) => `"${f.name}"`).join(", ") || defaultCols.join(", ")}`
|
|
538
|
+
);
|
|
539
|
+
yamlLines.push(` background: "background"`);
|
|
540
|
+
yamlLines.push("");
|
|
541
|
+
if (layout === "table") {
|
|
542
|
+
yamlLines.push(` - data_table:`);
|
|
543
|
+
yamlLines.push(` label: "${entities[0]}-table"`);
|
|
544
|
+
yamlLines.push(` heading:`);
|
|
545
|
+
yamlLines.push(` text: "Detailed View"`);
|
|
546
|
+
yamlLines.push(` textSize: "h3"`);
|
|
547
|
+
yamlLines.push(` collection: "${entities[0]}"`);
|
|
548
|
+
yamlLines.push(` sortableColumns: true`);
|
|
549
|
+
yamlLines.push(` exportable: true`);
|
|
550
|
+
yamlLines.push(` background: "surface"`);
|
|
551
|
+
}
|
|
552
|
+
const yaml = yamlLines.join("\n");
|
|
553
|
+
return {
|
|
554
|
+
content: [
|
|
555
|
+
{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: `\u{1F4CA} Generated Dashboard for: ${entities.join(", ")}
|
|
558
|
+
|
|
559
|
+
Layout: ${layout}
|
|
560
|
+
|
|
561
|
+
\`\`\`yaml
|
|
562
|
+
${yaml}
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
\u{1F4A1} Add this to pages/dashboard/content.yml and validate with stackwright_validate_pages.`
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
server2.tool(
|
|
572
|
+
"stackwright_pro_generate_detail_page",
|
|
573
|
+
"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.",
|
|
574
|
+
{
|
|
575
|
+
entity: import_zod4.z.string().describe('Entity slug (e.g., "equipment")'),
|
|
576
|
+
slugField: import_zod4.z.string().optional().describe('Field to use as URL slug (default: "id")'),
|
|
577
|
+
specPath: import_zod4.z.string().optional().describe("Path to OpenAPI spec for field details")
|
|
578
|
+
},
|
|
579
|
+
async ({ entity, slugField = "id", specPath }) => {
|
|
580
|
+
let fields = [];
|
|
581
|
+
if (specPath) {
|
|
582
|
+
const result = (0, import_cli_data_explorer2.listEntities)({ specPath });
|
|
583
|
+
if (result.success) {
|
|
584
|
+
const ent = result.entities.find((e) => e.slug === entity);
|
|
585
|
+
if (ent) {
|
|
586
|
+
fields = ent.fields;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const entityName = entity.charAt(0).toUpperCase() + entity.slice(1);
|
|
591
|
+
const yamlLines = [
|
|
592
|
+
`# Detail page for ${entityName}`,
|
|
593
|
+
"# Generated by Stackwright Pro MCP",
|
|
594
|
+
"",
|
|
595
|
+
"content:",
|
|
596
|
+
" meta:",
|
|
597
|
+
` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
|
|
598
|
+
"",
|
|
599
|
+
" content_items:",
|
|
600
|
+
" - main:",
|
|
601
|
+
' label: "detail-header"',
|
|
602
|
+
" heading:",
|
|
603
|
+
` text: "{{ ${entity}.${slugField} }}"`,
|
|
604
|
+
' textSize: "h1"',
|
|
605
|
+
" textBlocks:",
|
|
606
|
+
` - text: "Details for this ${entity}"`,
|
|
607
|
+
' textSize: "body1"',
|
|
608
|
+
' background: "primary"',
|
|
609
|
+
' color: "text"',
|
|
610
|
+
"",
|
|
611
|
+
" - grid:",
|
|
612
|
+
' label: "detail-fields"',
|
|
613
|
+
" columns: 2",
|
|
614
|
+
" items:"
|
|
615
|
+
];
|
|
616
|
+
const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
|
|
617
|
+
{ name: slugField, type: "string" },
|
|
618
|
+
{ name: "created_at", type: "datetime" },
|
|
619
|
+
{ name: "updated_at", type: "datetime" }
|
|
620
|
+
];
|
|
621
|
+
for (const field of displayFields) {
|
|
622
|
+
const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
623
|
+
yamlLines.push(` - card:`);
|
|
624
|
+
yamlLines.push(` label: "${field.name}-field"`);
|
|
625
|
+
yamlLines.push(` heading:`);
|
|
626
|
+
yamlLines.push(` text: "${label}"`);
|
|
627
|
+
yamlLines.push(` textSize: "h4"`);
|
|
628
|
+
yamlLines.push(` content:`);
|
|
629
|
+
yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
|
|
630
|
+
yamlLines.push(` textSize: "body1"`);
|
|
631
|
+
}
|
|
632
|
+
yamlLines.push(' background: "background"');
|
|
633
|
+
const yaml = yamlLines.join("\n");
|
|
634
|
+
return {
|
|
635
|
+
content: [
|
|
636
|
+
{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `\u{1F4C4} Generated Detail Page for: ${entity}
|
|
639
|
+
|
|
640
|
+
\`\`\`yaml
|
|
641
|
+
${yaml}
|
|
642
|
+
\`\`\`
|
|
643
|
+
|
|
644
|
+
\u{1F4A1} Add this to pages/${entity}/[${slugField}]/content.yml and validate.`
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/server.ts
|
|
653
|
+
var server = new import_mcp.McpServer({
|
|
654
|
+
name: "stackwright-pro",
|
|
655
|
+
version: "0.1.0"
|
|
656
|
+
});
|
|
657
|
+
registerDataExplorerTools(server);
|
|
658
|
+
registerSecurityTools(server);
|
|
659
|
+
registerIsrTools(server);
|
|
660
|
+
registerDashboardTools(server);
|
|
661
|
+
async function main() {
|
|
662
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
663
|
+
await server.connect(transport);
|
|
664
|
+
console.error("Stackwright Pro MCP server running on stdio");
|
|
665
|
+
}
|
|
666
|
+
main().catch((err) => {
|
|
667
|
+
console.error("Fatal:", err);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
});
|
|
670
|
+
//# sourceMappingURL=server.js.map
|