@stackwright-pro/mcp 0.2.0-alpha.111 → 0.2.0-alpha.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -556,7 +556,7 @@ function registerDashboardTools(server2) {
556
556
  yamlLines.push(` header: Status`);
557
557
  yamlLines.push(` type: badge`);
558
558
  }
559
- const yaml3 = yamlLines.join("\n");
559
+ const yaml4 = yamlLines.join("\n");
560
560
  return {
561
561
  content: [
562
562
  {
@@ -566,7 +566,7 @@ function registerDashboardTools(server2) {
566
566
  Layout: ${layout}
567
567
 
568
568
  \`\`\`yaml
569
- ${yaml3}
569
+ ${yaml4}
570
570
  \`\`\`
571
571
 
572
572
  \u{1F4A1} Add this to pages/dashboard/content.yml and validate with stackwright_validate_pages.`
@@ -647,7 +647,7 @@ ${yaml3}
647
647
  yamlLines.push(" action: navigate");
648
648
  yamlLines.push(` href: "/${entity}"`);
649
649
  yamlLines.push(" style: secondary");
650
- const yaml3 = yamlLines.join("\n");
650
+ const yaml4 = yamlLines.join("\n");
651
651
  return {
652
652
  content: [
653
653
  {
@@ -655,7 +655,7 @@ ${yaml3}
655
655
  text: `\u{1F4C4} Generated Detail Page for: ${entity}
656
656
 
657
657
  \`\`\`yaml
658
- ${yaml3}
658
+ ${yaml4}
659
659
  \`\`\`
660
660
 
661
661
  \u{1F4A1} Add this to pages/${entity}/[${slugField}]/content.yml and validate.`
@@ -2826,21 +2826,40 @@ function readAllManifestPages(artifactsDir) {
2826
2826
  return allPages;
2827
2827
  }
2828
2828
  for (const entry of entries) {
2829
- if (!entry.endsWith("-manifest.json")) continue;
2830
- const filePath = join5(artifactsDir, entry);
2831
- try {
2832
- const raw = JSON.parse(readFileSync5(filePath, "utf-8"));
2833
- if (!Array.isArray(raw.pages)) continue;
2834
- for (const page of raw.pages) {
2835
- if (typeof page.slug !== "string" || !page.slug) continue;
2829
+ if (entry.endsWith("-manifest.json")) {
2830
+ const filePath = join5(artifactsDir, entry);
2831
+ try {
2832
+ const raw = JSON.parse(readFileSync5(filePath, "utf-8"));
2833
+ if (!Array.isArray(raw.pages)) continue;
2834
+ for (const page of raw.pages) {
2835
+ if (typeof page.slug !== "string" || !page.slug) continue;
2836
+ allPages.push({
2837
+ slug: page.slug,
2838
+ authRequired: page.authRequired,
2839
+ requiredRoles: page.requiredRoles,
2840
+ source: entry
2841
+ });
2842
+ }
2843
+ } catch {
2844
+ }
2845
+ continue;
2846
+ }
2847
+ if (entry === "workflow-config.json" || entry.startsWith("workflow-config-")) {
2848
+ const filePath = join5(artifactsDir, entry);
2849
+ try {
2850
+ const raw = JSON.parse(readFileSync5(filePath, "utf-8"));
2851
+ const route = raw.workflow?.route;
2852
+ if (typeof route !== "string" || !route) continue;
2853
+ const slug = route.startsWith("/") ? route.slice(1) : route;
2836
2854
  allPages.push({
2837
- slug: page.slug,
2838
- authRequired: page.authRequired,
2839
- requiredRoles: page.requiredRoles,
2855
+ slug,
2856
+ authRequired: true,
2857
+ requiredRoles: void 0,
2840
2858
  source: entry
2841
2859
  });
2860
+ } catch {
2842
2861
  }
2843
- } catch {
2862
+ continue;
2844
2863
  }
2845
2864
  }
2846
2865
  return allPages;
@@ -2848,7 +2867,7 @@ function readAllManifestPages(artifactsDir) {
2848
2867
  function registerReadAuthManifestsTool(server2) {
2849
2868
  server2.tool(
2850
2869
  "stackwright_pro_read_auth_manifests",
2851
- "Read ALL *-manifest.json files from the pipeline artifacts directory (pages-manifest.json, dashboard-manifest.json, geo-manifest.json, etc.) and deterministically derive protectedRoutes and uncoveredSlugs. Uses the both-forms invariant (swp-n7kk): every authRequired route registers BOTH /{slug} (exact) AND /{slug}/:path* (sub-path). Call this in Step 1 of auth configuration instead of manually reading each manifest file. Fixes the Bead C issue where dashboard-manifest.json and geo-manifest.json were silently missed.",
2870
+ "Read ALL route-producing artifacts from the pipeline artifacts directory: *-manifest.json files (pages, dashboard, geo, form-wizard) AND workflow-config.json / workflow-config-*.json (different shape: workflow.route). Deterministically derives protectedRoutes and uncoveredSlugs. Uses the both-forms invariant (swp-n7kk): every authRequired route registers BOTH /{slug} (exact) AND /{slug}/:path* (sub-path). Workflow routes appear in uncoveredSlugs (no requiredRoles declared) for auth-reconcile to resolve via cross-cutting rules from auth-config.json. Extended by swp-hng0 to cover all four manifest types (was: pages+dashboard only).",
2852
2871
  {
2853
2872
  rbacRoles: z11.array(z11.string()).describe(
2854
2873
  'RBAC role hierarchy in descending privilege order, e.g. ["ESF8_COORDINATOR", "HOSPITAL_EM", "OBSERVER"]. Used to compute the lowestRole for each authRequired page.'
@@ -2994,7 +3013,7 @@ function enrichErrors(issues, synonyms) {
2994
3013
  });
2995
3014
  }
2996
3015
  function handleValidateYamlFragment(input) {
2997
- const { schemaName, yaml: yaml3 } = input;
3016
+ const { schemaName, yaml: yaml4 } = input;
2998
3017
  if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
2999
3018
  return {
3000
3019
  valid: false,
@@ -3004,7 +3023,7 @@ function handleValidateYamlFragment(input) {
3004
3023
  const name = schemaName;
3005
3024
  let parsed;
3006
3025
  try {
3007
- parsed = yamlLoad2(yaml3);
3026
+ parsed = yamlLoad2(yaml4);
3008
3027
  } catch (err) {
3009
3028
  return {
3010
3029
  valid: false,
@@ -3030,8 +3049,8 @@ function registerValidateYamlFragmentTool(server2) {
3030
3049
  ),
3031
3050
  yaml: z12.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
3032
3051
  },
3033
- async ({ schemaName, yaml: yaml3 }) => {
3034
- const result = handleValidateYamlFragment({ schemaName, yaml: yaml3 });
3052
+ async ({ schemaName, yaml: yaml4 }) => {
3053
+ const result = handleValidateYamlFragment({ schemaName, yaml: yaml4 });
3035
3054
  return {
3036
3055
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3037
3056
  };
@@ -3244,6 +3263,9 @@ var MANIFEST_NAME_TO_PHASE = {
3244
3263
  "stackwright-pro-dashboard-otter": "dashboard",
3245
3264
  "stackwright-pro-form-wizard-otter": "workflow",
3246
3265
  "stackwright-pro-geo-otter": "geo",
3266
+ // auth-reconcile runs AFTER pages+dashboard+geo+workflow (wave 6)
3267
+ // Reads auth-config.json + all four manifests; emits auth-reconcile-config.json + stackwright.auth.yml
3268
+ "stackwright-pro-auth-reconcile-otter": "auth-reconcile",
3247
3269
  "stackwright-pro-polish-otter": "polish",
3248
3270
  "stackwright-services-otter": "services",
3249
3271
  "stackwright-pro-qa-otter": "qa"
@@ -3459,13 +3481,19 @@ var PHASE_ORDER = [
3459
3481
  "api",
3460
3482
  "data",
3461
3483
  "auth",
3462
- // moved earlier only depends on design-language.json (designer)
3484
+ // wave 2 — moved earlier, only depends on design-language.json (designer)
3463
3485
  "geo",
3464
3486
  "workflow",
3465
3487
  "services",
3466
3488
  "pages",
3467
3489
  "dashboard",
3490
+ // auth-reconcile: wave 6 — post-manifest reconciliation
3491
+ // Reads auth-config.json + pages/dashboard/geo/workflow manifests;
3492
+ // emits auth-reconcile-config.json + stackwright.auth.yml (Gate 4 single source of truth)
3493
+ "auth-reconcile",
3494
+ // wave 7: polish — depends on auth-reconcile (replaces auth-config.json direct dep)
3468
3495
  "polish",
3496
+ // wave 8: qa — terminal QA gate, depends on polish + transitively everything
3469
3497
  "qa"
3470
3498
  ];
3471
3499
  var PHASE_ARTIFACT = {
@@ -3479,6 +3507,7 @@ var PHASE_ARTIFACT = {
3479
3507
  dashboard: "dashboard-manifest.json",
3480
3508
  workflow: "workflow-config.json",
3481
3509
  services: "services-config.json",
3510
+ "auth-reconcile": "auth-reconcile-config.json",
3482
3511
  polish: "polish-manifest.json",
3483
3512
  geo: "geo-manifest.json",
3484
3513
  qa: "qa-findings.json"
@@ -3493,16 +3522,36 @@ var PHASE_TO_OTTER2 = {
3493
3522
  pages: "stackwright-pro-page-otter",
3494
3523
  dashboard: "stackwright-pro-dashboard-otter",
3495
3524
  workflow: "stackwright-pro-form-wizard-otter",
3525
+ "auth-reconcile": "stackwright-pro-auth-reconcile-otter",
3496
3526
  services: "stackwright-services-otter",
3497
3527
  polish: "stackwright-pro-polish-otter",
3498
3528
  geo: "stackwright-pro-geo-otter",
3499
3529
  qa: "stackwright-pro-qa-otter"
3500
3530
  };
3531
+ var AUTO_EXECUTE_PHASES = /* @__PURE__ */ new Set(["auth-reconcile"]);
3532
+ var PhaseStatusSchema = z14.object({
3533
+ status: z14.enum(["pending", "running", "completed", "skipped", "failed"]).default("pending"),
3534
+ questionsCollected: z14.boolean().default(false),
3535
+ answered: z14.boolean().default(false),
3536
+ executed: z14.boolean().default(false),
3537
+ artifactWritten: z14.boolean().default(false),
3538
+ retryCount: z14.number().default(0),
3539
+ inFlight: z14.boolean().default(false)
3540
+ });
3541
+ var PipelineStateSchema = z14.object({
3542
+ version: z14.literal("1.0"),
3543
+ currentPhase: z14.string(),
3544
+ status: z14.enum(["setup", "questions", "execution", "done"]),
3545
+ phases: z14.record(z14.string(), PhaseStatusSchema),
3546
+ startedAt: z14.string(),
3547
+ updatedAt: z14.string()
3548
+ });
3501
3549
  function isValidPhase2(phase) {
3502
3550
  return PHASE_ORDER.includes(phase);
3503
3551
  }
3504
3552
  function defaultPhaseStatus() {
3505
3553
  return {
3554
+ status: "pending",
3506
3555
  questionsCollected: false,
3507
3556
  answered: false,
3508
3557
  executed: false,
@@ -3533,10 +3582,9 @@ function readState(cwd) {
3533
3582
  const p = statePath(cwd);
3534
3583
  if (!existsSync7(p)) return createDefaultState();
3535
3584
  const raw = JSON.parse(safeReadSync(p));
3536
- if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
3537
- return createDefaultState();
3538
- }
3539
- return raw;
3585
+ const result = PipelineStateSchema.safeParse(raw);
3586
+ if (!result.success) return createDefaultState();
3587
+ return result.data;
3540
3588
  }
3541
3589
  function safeWriteSync(filePath, content) {
3542
3590
  if (existsSync7(filePath)) {
@@ -3625,13 +3673,16 @@ function handleSetPipelineState(input) {
3625
3673
  isError: true
3626
3674
  };
3627
3675
  }
3628
- const VALID_FIELDS = [
3629
- "questionsCollected",
3630
- "answered",
3631
- "executed",
3632
- "artifactWritten",
3633
- "inFlight"
3634
- ];
3676
+ if (input.field === "artifactWritten") {
3677
+ return {
3678
+ text: JSON.stringify({
3679
+ error: true,
3680
+ message: "Cannot set 'artifactWritten' directly. Use stackwright_pro_validate_artifact instead \u2014 it verifies the file exists on disk before flagging."
3681
+ }),
3682
+ isError: true
3683
+ };
3684
+ }
3685
+ const VALID_FIELDS = ["questionsCollected", "answered", "executed", "inFlight"];
3635
3686
  if (input.field && !VALID_FIELDS.includes(input.field)) {
3636
3687
  return {
3637
3688
  text: JSON.stringify({
@@ -3652,6 +3703,15 @@ function handleSetPipelineState(input) {
3652
3703
  isError: true
3653
3704
  };
3654
3705
  }
3706
+ if (update.field === "artifactWritten") {
3707
+ return {
3708
+ text: JSON.stringify({
3709
+ error: true,
3710
+ message: "Cannot set 'artifactWritten' directly. Use stackwright_pro_validate_artifact instead \u2014 it verifies the file exists on disk before flagging."
3711
+ }),
3712
+ isError: true
3713
+ };
3714
+ }
3655
3715
  if (!VALID_FIELDS.includes(update.field)) {
3656
3716
  return {
3657
3717
  text: JSON.stringify({
@@ -3677,6 +3737,9 @@ function handleSetPipelineState(input) {
3677
3737
  if (input.field && input.value !== void 0) {
3678
3738
  phaseState[input.field] = input.value;
3679
3739
  }
3740
+ if (input.phaseStatus !== void 0) {
3741
+ phaseState.status = input.phaseStatus;
3742
+ }
3680
3743
  if (input.incrementRetry) {
3681
3744
  phaseState.retryCount += 1;
3682
3745
  }
@@ -3703,16 +3766,6 @@ function handleSetPipelineState(input) {
3703
3766
  },
3704
3767
  { cwd }
3705
3768
  );
3706
- if (input.field === "artifactWritten" && input.value === true && input.phase) {
3707
- emit({ type: "phase_complete", phase: input.phase }, { cwd });
3708
- }
3709
- if (input.updates) {
3710
- for (const update of input.updates) {
3711
- if (update.field === "artifactWritten" && update.value === true) {
3712
- emit({ type: "phase_complete", phase: update.phase }, { cwd });
3713
- }
3714
- }
3715
- }
3716
3769
  } catch {
3717
3770
  }
3718
3771
  return { text: JSON.stringify(state), isError: false };
@@ -3732,6 +3785,9 @@ function handleCheckExecutionReady(_cwd, phase) {
3732
3785
  isError: true
3733
3786
  };
3734
3787
  }
3788
+ if (AUTO_EXECUTE_PHASES.has(phase)) {
3789
+ return { text: JSON.stringify({ ready: true, phase, autoExecute: true }), isError: false };
3790
+ }
3735
3791
  const answerFile = join7(cwd, ".stackwright", "answers", `${phase}.json`);
3736
3792
  if (!existsSync7(answerFile)) {
3737
3793
  return {
@@ -3761,6 +3817,10 @@ function handleCheckExecutionReady(_cwd, phase) {
3761
3817
  const answeredPhases = [];
3762
3818
  const missingPhases = [];
3763
3819
  for (const phase2 of PHASE_ORDER) {
3820
+ if (AUTO_EXECUTE_PHASES.has(phase2)) {
3821
+ answeredPhases.push(phase2);
3822
+ continue;
3823
+ }
3764
3824
  const answerFile = join7(answersDir, `${phase2}.json`);
3765
3825
  if (existsSync7(answerFile)) {
3766
3826
  try {
@@ -4089,6 +4149,8 @@ var PHASE_REQUIRED_KEYS = {
4089
4149
  dashboard: ["version", "generatedBy"],
4090
4150
  workflow: ["version", "generatedBy"],
4091
4151
  services: ["version", "generatedBy", "flows"],
4152
+ // auth-reconcile: must have version, generatedBy, and merged protectedRoutes
4153
+ "auth-reconcile": ["version", "generatedBy", "protectedRoutes"],
4092
4154
  polish: ["version", "generatedBy"],
4093
4155
  geo: ["version", "generatedBy", "geoCollections"],
4094
4156
  // qa: skipped=true path only needs version+generatedBy+skipped;
@@ -4368,6 +4430,28 @@ var PHASE_ARTIFACT_SCHEMA = {
4368
4430
  null,
4369
4431
  2
4370
4432
  ),
4433
+ "auth-reconcile": JSON.stringify(
4434
+ {
4435
+ version: "1.0",
4436
+ generatedBy: "stackwright-pro-auth-reconcile-otter",
4437
+ authType: "<oidc|saml|cac|oauth2>",
4438
+ rbacRoles: ["<HIGHEST_ROLE>", "<MID_ROLE>", "<LOWEST_ROLE>"],
4439
+ rbacDefaultRole: "<LOWEST_ROLE>",
4440
+ protectedRoutes: [
4441
+ { pattern: "/dashboard", requiredRole: "<role>" },
4442
+ { pattern: "/dashboard/:path*", requiredRole: "<role>" }
4443
+ ],
4444
+ uncoveredSlugs: [],
4445
+ manifestsRead: ["pages-manifest.json", "dashboard-manifest.json", "geo-manifest.json"],
4446
+ summary: {
4447
+ totalRoutes: 2,
4448
+ manifestDerived: 2,
4449
+ crossCutting: 0
4450
+ }
4451
+ },
4452
+ null,
4453
+ 2
4454
+ ),
4371
4455
  polish: JSON.stringify(
4372
4456
  {
4373
4457
  version: "1.0",
@@ -4791,6 +4875,7 @@ function _validateArtifactInner(input) {
4791
4875
  if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
4792
4876
  const ps = state.phases[phase];
4793
4877
  ps.artifactWritten = true;
4878
+ ps.status = "completed";
4794
4879
  });
4795
4880
  const topKeys = Object.keys(artifact).slice(0, 5).join(", ");
4796
4881
  const result = {
@@ -4818,6 +4903,7 @@ function handleValidateArtifact(input) {
4818
4903
  const parsed = JSON.parse(result.text);
4819
4904
  if (parsed.valid === true && parsed.artifactPath) {
4820
4905
  emit({ type: "file_write", path: parsed.artifactPath, phase }, { cwd });
4906
+ emit({ type: "phase_complete", phase }, { cwd });
4821
4907
  }
4822
4908
  emit(
4823
4909
  {
@@ -4918,12 +5004,19 @@ function registerPipelineTools(server2) {
4918
5004
  `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
4919
5005
  {
4920
5006
  phase: z14.string().optional().describe('Phase to update, e.g. "designer"'),
4921
- field: z14.enum(["questionsCollected", "answered", "executed", "artifactWritten", "inFlight"]).optional().describe("Boolean field to set"),
5007
+ field: z14.enum(["questionsCollected", "answered", "executed", "inFlight"]).optional().describe(
5008
+ "Boolean field to set. NOTE: artifactWritten is intentionally absent \u2014 use stackwright_pro_validate_artifact instead (it verifies the file exists on disk before flagging)."
5009
+ ),
4922
5010
  // inFlight: set true BEFORE specialist invoke (dataflow mode, swp-ioc7); false after.
4923
5011
  value: boolCoerce(z14.boolean().optional()).describe(
4924
5012
  'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
4925
5013
  ),
4926
- status: z14.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
5014
+ // phaseStatus: per-phase execution enum DISTINCT from top-level `status` (pipeline flow).
5015
+ // Named `phaseStatus` to avoid ambiguity. Sets PhaseStatus.status on the given phase.
5016
+ phaseStatus: z14.enum(["pending", "running", "completed", "skipped", "failed"]).optional().describe(
5017
+ "Per-phase execution status (pending/running/completed/skipped/failed). Distinct from top-level `status` (pipeline flow). Use phaseStatus: 'skipped' when a specialist otter is unregistered."
5018
+ ),
5019
+ status: z14.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level pipeline flow status override \u2014 distinct from phaseStatus"),
4927
5020
  incrementRetry: boolCoerce(z14.boolean().optional()).describe(
4928
5021
  "Bump retryCount by 1 \u2014 must be a JSON boolean"
4929
5022
  ),
@@ -4935,8 +5028,8 @@ function registerPipelineTools(server2) {
4935
5028
  "questionsCollected",
4936
5029
  "answered",
4937
5030
  "executed",
4938
- "artifactWritten",
4939
5031
  "inFlight"
5032
+ // artifactWritten intentionally absent — use validate_artifact (swp-og9c)
4940
5033
  ]),
4941
5034
  value: z14.boolean()
4942
5035
  })
@@ -4948,6 +5041,7 @@ function registerPipelineTools(server2) {
4948
5041
  ...args.phase != null ? { phase: args.phase } : {},
4949
5042
  ...args.field != null ? { field: args.field } : {},
4950
5043
  ...args.value != null ? { value: args.value } : {},
5044
+ ...args.phaseStatus != null ? { phaseStatus: args.phaseStatus } : {},
4951
5045
  ...args.status != null ? { status: args.status } : {},
4952
5046
  ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
4953
5047
  ...args.updates != null ? { updates: args.updates } : {}
@@ -5109,6 +5203,20 @@ var OTTER_WRITE_ALLOWLISTS = {
5109
5203
  description: "Stackwright config (themeName + customTheme writeback)"
5110
5204
  }
5111
5205
  ],
5206
+ // swp-hng0: auth-reconcile otter — reads all manifests, re-emits auth-reconcile-config.json
5207
+ // AND overwrites stackwright.auth.yml (Gate 4: single source of truth after reconciliation)
5208
+ "stackwright-pro-auth-reconcile-otter": [
5209
+ {
5210
+ prefix: ".stackwright/artifacts/",
5211
+ suffix: ".json",
5212
+ description: "Auth reconcile artifact (auth-reconcile-config.json)"
5213
+ },
5214
+ {
5215
+ prefix: "stackwright.auth.",
5216
+ suffix: ".yml",
5217
+ description: "Canonical auth sidecar (stackwright.auth.yml \u2014 Gate 4 rewrite)"
5218
+ }
5219
+ ],
5112
5220
  "stackwright-pro-auth-otter": [
5113
5221
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
5114
5222
  {
@@ -6532,7 +6640,11 @@ var _checksums = /* @__PURE__ */ new Map([
6532
6640
  ],
6533
6641
  [
6534
6642
  "stackwright-pro-auth-otter.json",
6535
- "eee511fa214203ce88d51996c6b1b483b6ad3a0ae474cc7be5d380fdbbc43bb1"
6643
+ "c9302132ac4180c70dd452814eb4aa4fb9a1d6a9683f6e01ffce509a6001c6cf"
6644
+ ],
6645
+ [
6646
+ "stackwright-pro-auth-reconcile-otter.json",
6647
+ "7ffd67d51568c7b2a1eb2ea0ac2b2a8041eb9ddd9c1eb5b95744e55ba049b385"
6536
6648
  ],
6537
6649
  [
6538
6650
  "stackwright-pro-dashboard-otter.json",
@@ -6540,7 +6652,7 @@ var _checksums = /* @__PURE__ */ new Map([
6540
6652
  ],
6541
6653
  [
6542
6654
  "stackwright-pro-data-otter.json",
6543
- "be4e3d6f530ae2d6b79d6412e8d2d6defb3caffddedf5be4f928976a72206074"
6655
+ "7d1de846488d84eebf8798aef4d75f4f4d250c1ae070ce1200ead3bb8a545eaf"
6544
6656
  ],
6545
6657
  [
6546
6658
  "stackwright-pro-designer-otter.json",
@@ -6552,7 +6664,7 @@ var _checksums = /* @__PURE__ */ new Map([
6552
6664
  ],
6553
6665
  [
6554
6666
  "stackwright-pro-foreman-otter.json",
6555
- "88c4101154d44a7c5470ded13562402a39323ee570ed3a90a73b8d85d7fd2817"
6667
+ "65aa3273274b2b1859d432c3d1166c4a73971861036bf18c35acd8a5357fcf26"
6556
6668
  ],
6557
6669
  [
6558
6670
  "stackwright-pro-form-wizard-otter.json",
@@ -6568,7 +6680,7 @@ var _checksums = /* @__PURE__ */ new Map([
6568
6680
  ],
6569
6681
  [
6570
6682
  "stackwright-pro-polish-otter.json",
6571
- "4b8223d89af7f92b9051fee7bb29fc4b1ee2a5a9ab87e8e67a6d8be6122fa029"
6683
+ "3ae1252a60727ebca67e33a807d5061cafb88de52f3b98a9d4641f9db725a6ed"
6572
6684
  ],
6573
6685
  [
6574
6686
  "stackwright-pro-qa-otter.json",
@@ -8482,12 +8594,181 @@ function registerListSpecsTool(server2) {
8482
8594
  );
8483
8595
  }
8484
8596
 
8485
- // src/tools/consolidate-integrations.ts
8597
+ // src/tools/describe-endpoint.ts
8486
8598
  import { z as z25 } from "zod";
8487
- import { readdirSync as readdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10, existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
8599
+ import { readFileSync as readFileSync16 } from "fs";
8600
+ import { extname as extname2 } from "path";
8601
+ import yaml2 from "js-yaml";
8602
+ function resolveRef(root, ref) {
8603
+ if (!ref.startsWith("#/")) {
8604
+ throw new Error(`[describe_endpoint] External $refs not supported: "${ref}"`);
8605
+ }
8606
+ const parts = ref.slice(2).split("/");
8607
+ let current = root;
8608
+ for (const part of parts) {
8609
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
8610
+ if (current == null || typeof current !== "object") {
8611
+ throw new Error(
8612
+ `[describe_endpoint] $ref "${ref}" cannot be resolved \u2014 segment "${key}" not found`
8613
+ );
8614
+ }
8615
+ current = current[key];
8616
+ }
8617
+ if (current == null || typeof current !== "object") {
8618
+ throw new Error(`[describe_endpoint] $ref "${ref}" resolved to a non-object value`);
8619
+ }
8620
+ return current;
8621
+ }
8622
+ function deref(root, obj) {
8623
+ if (obj == null || typeof obj !== "object") return {};
8624
+ const o = obj;
8625
+ if (typeof o["$ref"] === "string") {
8626
+ const resolved = resolveRef(root, o["$ref"]);
8627
+ return deref(root, resolved);
8628
+ }
8629
+ return o;
8630
+ }
8631
+ function loadSpec(specPath) {
8632
+ const raw = readFileSync16(specPath, "utf-8");
8633
+ const ext = extname2(specPath).toLowerCase();
8634
+ if (ext === ".json") {
8635
+ return JSON.parse(raw);
8636
+ }
8637
+ const parsed = yaml2.load(raw);
8638
+ if (parsed == null || typeof parsed !== "object") {
8639
+ throw new Error(`[describe_endpoint] Spec file "${specPath}" did not parse to an object`);
8640
+ }
8641
+ return parsed;
8642
+ }
8643
+ function extractParams(root, pathItem, operation) {
8644
+ const pathLevelRaw = Array.isArray(pathItem["parameters"]) ? pathItem["parameters"] : [];
8645
+ const opLevelRaw = Array.isArray(operation["parameters"]) ? operation["parameters"] : [];
8646
+ const merged = /* @__PURE__ */ new Map();
8647
+ for (const raw of [...pathLevelRaw, ...opLevelRaw]) {
8648
+ const param = deref(root, raw);
8649
+ const name = String(param["name"] ?? "");
8650
+ const inVal = String(param["in"] ?? "");
8651
+ if (name && inVal) {
8652
+ merged.set(`${name}:${inVal}`, param);
8653
+ }
8654
+ }
8655
+ const pathParams = [];
8656
+ const queryParams = [];
8657
+ for (const param of merged.values()) {
8658
+ const inVal = String(param["in"] ?? "");
8659
+ const name = String(param["name"] ?? "");
8660
+ const schema = deref(root, param["schema"] ?? {});
8661
+ if (inVal === "path") {
8662
+ pathParams.push({ name, in: "path", required: true, schema });
8663
+ } else if (inVal === "query") {
8664
+ const required = param["required"] === true;
8665
+ queryParams.push({ name, in: "query", required, schema });
8666
+ }
8667
+ }
8668
+ return { pathParams, queryParams };
8669
+ }
8670
+ function extractSecurity(root, globalSecurity, operationSecurity) {
8671
+ const securityRequirements = operationSecurity !== void 0 ? operationSecurity : globalSecurity;
8672
+ if (securityRequirements.length === 0) return [];
8673
+ const schemes = root["components"]?.["securitySchemes"];
8674
+ if (!schemes || typeof schemes !== "object") return [];
8675
+ const result = [];
8676
+ const seen = /* @__PURE__ */ new Set();
8677
+ for (const req of securityRequirements) {
8678
+ if (!req || typeof req !== "object") continue;
8679
+ for (const schemeName of Object.keys(req)) {
8680
+ if (seen.has(schemeName)) continue;
8681
+ seen.add(schemeName);
8682
+ const schemeRaw = schemes[schemeName];
8683
+ if (!schemeRaw) continue;
8684
+ const scheme = deref(root, schemeRaw);
8685
+ const type = String(scheme["type"] ?? "unknown");
8686
+ if (type === "apiKey") {
8687
+ const inVal = String(scheme["in"] ?? "header");
8688
+ const paramName = String(scheme["name"] ?? schemeName);
8689
+ result.push({ name: schemeName, type, in: inVal, paramName });
8690
+ } else if (type === "http") {
8691
+ const httpScheme = String(scheme["scheme"] ?? "bearer").toLowerCase();
8692
+ result.push({ name: schemeName, type, in: "bearer", paramName: "Authorization" });
8693
+ void httpScheme;
8694
+ } else if (type === "oauth2") {
8695
+ result.push({ name: schemeName, type, in: "bearer", paramName: "Authorization" });
8696
+ } else if (type === "openIdConnect") {
8697
+ result.push({ name: schemeName, type, in: "bearer", paramName: "Authorization" });
8698
+ } else {
8699
+ result.push({ name: schemeName, type, in: "unknown", paramName: schemeName });
8700
+ }
8701
+ }
8702
+ }
8703
+ return result;
8704
+ }
8705
+ function handleDescribeEndpoint(input) {
8706
+ const { specPath, path: path5, method } = input;
8707
+ const normalizedMethod = method.toLowerCase();
8708
+ const root = loadSpec(specPath);
8709
+ const globalSecurity = Array.isArray(root["security"]) ? root["security"] : [];
8710
+ const paths = root["paths"];
8711
+ if (!paths) {
8712
+ return { pathParams: [], queryParams: [], security: [] };
8713
+ }
8714
+ const pathItem = deref(root, paths[path5]);
8715
+ if (!pathItem || Object.keys(pathItem).length === 0) {
8716
+ throw new Error(`[describe_endpoint] Path "${path5}" not found in spec "${specPath}"`);
8717
+ }
8718
+ const operation = deref(root, pathItem[normalizedMethod]);
8719
+ if (!operation || Object.keys(operation).length === 0) {
8720
+ throw new Error(
8721
+ `[describe_endpoint] Method "${method.toUpperCase()}" not found at path "${path5}" in spec "${specPath}"`
8722
+ );
8723
+ }
8724
+ const { pathParams, queryParams } = extractParams(root, pathItem, operation);
8725
+ const operationSecurity = Array.isArray(operation["security"]) ? operation["security"] : void 0;
8726
+ const security = extractSecurity(root, globalSecurity, operationSecurity);
8727
+ return { pathParams, queryParams, security };
8728
+ }
8729
+ function registerDescribeEndpointTool(server2) {
8730
+ server2.tool(
8731
+ "stackwright_pro_describe_endpoint",
8732
+ [
8733
+ "Inspect an OpenAPI 3.0/3.1 spec and return resolved parameter and security details for a",
8734
+ "specific operation. Deterministic \u2014 no LLM. Handles $ref resolution within the spec document.",
8735
+ "",
8736
+ "Returns:",
8737
+ " pathParams \u2014 path parameters [{name, in, required, schema}]",
8738
+ " queryParams \u2014 query parameters [{name, in, required, schema}]",
8739
+ " security \u2014 applicable security schemes [{name, type, in, paramName}]",
8740
+ "",
8741
+ "Used by the data-otter to populate params: entries in stackwright.collections.yml",
8742
+ "from required query params and resolved auth query params (swp-8r8i + swp-1l6l)."
8743
+ ].join("\n"),
8744
+ {
8745
+ specPath: z25.string().describe("Absolute path to the OpenAPI spec file (.yaml, .yml, or .json)"),
8746
+ path: z25.string().describe('The API path to inspect (e.g. "/stations/{stationId}/observations")'),
8747
+ method: z25.string().describe('HTTP method (case-insensitive: "get", "post", "put", etc.)')
8748
+ },
8749
+ async ({ specPath, path: path5, method }) => {
8750
+ try {
8751
+ const result = handleDescribeEndpoint({ specPath, path: path5, method });
8752
+ return {
8753
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
8754
+ };
8755
+ } catch (err) {
8756
+ const message = err instanceof Error ? err.message : String(err);
8757
+ return {
8758
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
8759
+ isError: true
8760
+ };
8761
+ }
8762
+ }
8763
+ );
8764
+ }
8765
+
8766
+ // src/tools/consolidate-integrations.ts
8767
+ import { z as z26 } from "zod";
8768
+ import { readdirSync as readdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
8488
8769
  import { lockSync as lockSync2 } from "proper-lockfile";
8489
8770
  import { join as join17, basename as basename3 } from "path";
8490
- import yaml2 from "js-yaml";
8771
+ import yaml3 from "js-yaml";
8491
8772
  var BASE_MOCK_PORT = 4011;
8492
8773
  var STRIP_SUFFIXES2 = ["-openapi", "-asyncapi", "-api", "-spec"];
8493
8774
  function deriveNameFromFilename(filename) {
@@ -8520,7 +8801,7 @@ function handleConsolidateIntegrations(input) {
8520
8801
  entries.forEach((filename, index) => {
8521
8802
  let artifact = {};
8522
8803
  try {
8523
- const raw = readFileSync16(join17(artifactsDir, filename), "utf-8");
8804
+ const raw = readFileSync17(join17(artifactsDir, filename), "utf-8");
8524
8805
  artifact = JSON.parse(raw);
8525
8806
  } catch {
8526
8807
  }
@@ -8557,8 +8838,8 @@ function handleConsolidateIntegrations(input) {
8557
8838
  let existing = {};
8558
8839
  if (existsSync16(integrationsYmlPath)) {
8559
8840
  try {
8560
- const raw = readFileSync16(integrationsYmlPath, "utf-8");
8561
- existing = yaml2.load(raw) ?? {};
8841
+ const raw = readFileSync17(integrationsYmlPath, "utf-8");
8842
+ existing = yaml3.load(raw) ?? {};
8562
8843
  } catch {
8563
8844
  existing = {};
8564
8845
  }
@@ -8575,7 +8856,7 @@ function handleConsolidateIntegrations(input) {
8575
8856
  try {
8576
8857
  const header = `# stackwright.integrations.yml \u2014 Auto-generated by stackwright_pro_consolidate_integrations
8577
8858
  `;
8578
- const body = yaml2.dump(merged, { lineWidth: 120, noRefs: true });
8859
+ const body = yaml3.dump(merged, { lineWidth: 120, noRefs: true });
8579
8860
  writeFileSync10(integrationsYmlPath, header + body, "utf-8");
8580
8861
  } finally {
8581
8862
  release();
@@ -8604,7 +8885,7 @@ function registerConsolidateIntegrationsTool(server2) {
8604
8885
  "no per-spec artifacts are found."
8605
8886
  ].join(" "),
8606
8887
  {
8607
- projectRoot: z25.string().describe("Absolute path to the project root directory")
8888
+ projectRoot: z26.string().describe("Absolute path to the project root directory")
8608
8889
  },
8609
8890
  async ({ projectRoot }) => {
8610
8891
  try {
@@ -8662,7 +8943,7 @@ var package_default = {
8662
8943
  "test:coverage": "vitest run --coverage"
8663
8944
  },
8664
8945
  name: "@stackwright-pro/mcp",
8665
- version: "0.2.0-alpha.111",
8946
+ version: "0.2.0-alpha.113",
8666
8947
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
8667
8948
  license: "SEE LICENSE IN LICENSE",
8668
8949
  main: "./dist/server.js",
@@ -8745,6 +9026,7 @@ registerCompileTools(server);
8745
9026
  registerStripLegacyIntegrationsTool(server);
8746
9027
  registerEmitterTools(server);
8747
9028
  registerListSpecsTool(server);
9029
+ registerDescribeEndpointTool(server);
8748
9030
  registerConsolidateIntegrationsTool(server);
8749
9031
  registerContentTypeTools(server);
8750
9032
  registerPageTools(server);