gasp-standard-mcp 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # gasp-standard-mcp
2
2
 
3
- MCP server for the [GASP Standard](https://gaspwiki.com) — the Generally Accepted SaaS Performance standard for metric definitions, formulas, benchmarks, and relationships.
3
+ MCP server for the [GASP Standard](https://gaspwiki.com) — the Generally Accepted SaaS Principles standard for metric definitions, formulas, benchmarks, and relationships.
4
4
 
5
5
  Works with Claude Code, Claude Desktop, or any [MCP](https://modelcontextprotocol.io/)-compatible client.
6
6
 
@@ -42,10 +42,10 @@ Restart your MCP client after adding the config.
42
42
 
43
43
  ## Data
44
44
 
45
- - 299 metrics across 13 departments
46
- - 79 glossary definitions
45
+ - 300 metrics across 13 departments
46
+ - 80 glossary definitions
47
47
  - 22 relationship nodes, 34 edges
48
- - 75 data requirements with field types, grain, and example SQL
48
+ - 78 data requirements with field types, grain, and example SQL
49
49
  - Sources: the canonical [GASP Standard](https://gaspwiki.com) files
50
50
 
51
51
  ## Examples
package/build/index.js CHANGED
@@ -94,6 +94,41 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
94
94
  if (metric.aliases.length > 0) {
95
95
  parts.push(`\n**Aliases:** ${metric.aliases.join(", ")}`);
96
96
  }
97
+ // Dual-lens content
98
+ if (metric.dualLens) {
99
+ const dl = metric.dualLens;
100
+ parts.push("\n## Dual-Lens: Operating / Market Forms");
101
+ parts.push(`\n### Operating Form: ${dl.operating.label}`);
102
+ if (dl.operating.description)
103
+ parts.push(dl.operating.description);
104
+ if (dl.operating.celSource)
105
+ parts.push(`**CEL Source:** ${dl.operating.celSource}`);
106
+ if (dl.operating.atlInclusion)
107
+ parts.push(`**ATL Inclusion:** ${dl.operating.atlInclusion}`);
108
+ if (dl.operating.formula)
109
+ parts.push("```\n" + dl.operating.formula + "\n```");
110
+ if (dl.operating.insight)
111
+ parts.push(`**What it tells you:** ${dl.operating.insight}`);
112
+ parts.push(`\n### Market Form: ${dl.market.label}`);
113
+ if (dl.market.description)
114
+ parts.push(dl.market.description);
115
+ if (dl.market.celSource)
116
+ parts.push(`**CEL Source:** ${dl.market.celSource}`);
117
+ if (dl.market.atlInclusion)
118
+ parts.push(`**ATL Inclusion:** ${dl.market.atlInclusion}`);
119
+ if (dl.market.formula)
120
+ parts.push("```\n" + dl.market.formula + "\n```");
121
+ if (dl.market.insight)
122
+ parts.push(`**What it tells you:** ${dl.market.insight}`);
123
+ parts.push("\n### Bridge");
124
+ if (dl.bridge.reconciliation)
125
+ parts.push("**Reconciliation:**\n```\n" + dl.bridge.reconciliation + "\n```");
126
+ if (dl.bridge.diagnostic)
127
+ parts.push(`**Diagnostic:** ${dl.bridge.diagnostic}`);
128
+ }
129
+ if (metric.dualLensNote) {
130
+ parts.push(`\n## Dual-Lens Note\n\n${metric.dualLensNote}`);
131
+ }
97
132
  // Swap EOM → GASP branding
98
133
  const text = parts.join("\n").replace(/\bEOM Standard\b/g, "GASP Standard");
99
134
  return { content: [{ type: "text", text }] };
package/build/parser.js CHANGED
@@ -132,6 +132,7 @@ function parseSectionFields(name, slug, raw, department, category, aliases) {
132
132
  const formulas = extractFormulas(raw);
133
133
  const commonMistakes = extractListField(raw, "Common mistakes");
134
134
  const sources = extractSources(raw);
135
+ const { dualLens, dualLensNote } = parseDualLens(raw);
135
136
  return {
136
137
  name,
137
138
  slug,
@@ -147,6 +148,8 @@ function parseSectionFields(name, slug, raw, department, category, aliases) {
147
148
  rawMarkdown: `### ${name}\n\n${raw}`,
148
149
  signalType: "operational",
149
150
  signalTypeSecondary: null,
151
+ dualLens,
152
+ dualLensNote,
150
153
  };
151
154
  }
152
155
  /** Extract a **FieldName:** value from markdown */
@@ -203,6 +206,112 @@ function extractSources(text) {
203
206
  }
204
207
  return sources;
205
208
  }
209
+ /**
210
+ * Extract a field value from a dual-lens subsection.
211
+ * Terminates at next bold field (**X), code fence, or #### heading — not at \n\n.
212
+ */
213
+ function extractSubsectionField(text, fieldName) {
214
+ const fence = "\\`\\`\\`";
215
+ const pattern = new RegExp("\\*\\*" + fieldName + ":\\*\\*\\s*([\\s\\S]*?)(?=\\n\\*\\*[A-Z]|\\n" + fence + "|\\n####|$)", "");
216
+ const match = text.match(pattern);
217
+ return match ? match[1].trim() : "";
218
+ }
219
+ /** Extract the first code block from text */
220
+ function extractFirstCodeBlock(text) {
221
+ const fence = "```";
222
+ const regex = new RegExp(fence + "\\n?([\\s\\S]*?)" + fence);
223
+ const match = text.match(regex);
224
+ return match ? match[1].trim() : null;
225
+ }
226
+ /**
227
+ * Parse dual-lens content from a metric's raw markdown.
228
+ * Returns { dualLens, dualLensNote }.
229
+ *
230
+ * Pattern 1 — Standard O/M: #### Operating Form: ... / #### Market Form: ... / #### Bridge:
231
+ * Pattern 2 — Shared Churn: #### Dual-Lens: ... with **Operating Form:** / **Market Form:** bold labels
232
+ * Pattern 3 — Note only: #### Dual-Lens Note
233
+ */
234
+ function parseDualLens(raw) {
235
+ // --- Pattern 3: Note only ---
236
+ const noteHeadingMatch = raw.match(/#### Dual-Lens Note\s*\n([\s\S]*?)(?=\n---|\n####|\n###|$)/);
237
+ if (noteHeadingMatch) {
238
+ const noteText = noteHeadingMatch[1].trim();
239
+ return { dualLens: null, dualLensNote: noteText || null };
240
+ }
241
+ // --- Pattern 1: Standard O/M ---
242
+ const opHeadingMatch = raw.match(/#### Operating Form:\s*(.+)/);
243
+ if (opHeadingMatch) {
244
+ // Split into Operating, Market, Bridge sections
245
+ const opStart = raw.indexOf(opHeadingMatch[0]);
246
+ const mktMatch = raw.match(/#### Market Form:\s*(.+)/);
247
+ const bridgeMatch = raw.match(/#### Bridge:/);
248
+ if (!mktMatch || !bridgeMatch) {
249
+ return { dualLens: null, dualLensNote: null };
250
+ }
251
+ const mktStart = raw.indexOf(mktMatch[0]);
252
+ const bridgeStart = raw.indexOf(bridgeMatch[0]);
253
+ const opSection = raw.substring(opStart + opHeadingMatch[0].length, mktStart).trim();
254
+ const mktSection = raw.substring(mktStart + mktMatch[0].length, bridgeStart).trim();
255
+ const bridgeSection = raw.substring(bridgeStart + bridgeMatch[0].length).trim();
256
+ const operating = parseStandardForm(opHeadingMatch[1].trim(), opSection);
257
+ const market = parseStandardForm(mktMatch[1].trim(), mktSection);
258
+ const bridge = parseBridgeSection(bridgeSection);
259
+ return { dualLens: { operating, market, bridge }, dualLensNote: null };
260
+ }
261
+ // --- Pattern 2: Shared Churn ---
262
+ const dualLensHeadingMatch = raw.match(/#### Dual-Lens:/);
263
+ if (dualLensHeadingMatch) {
264
+ const dlStart = raw.indexOf(dualLensHeadingMatch[0]);
265
+ const dlSection = raw.substring(dlStart + dualLensHeadingMatch[0].length).trim();
266
+ // Split on bold Operating Form / Market Form / Bridge labels
267
+ const opBoldMatch = dlSection.match(/\*\*Operating Form:\s*([^*]+)\*\*/);
268
+ const mktBoldMatch = dlSection.match(/\*\*Market Form:\s*([^*]+)\*\*/);
269
+ const bridgeBoldMatch = dlSection.match(/\*\*Bridge:\*\*/);
270
+ if (!opBoldMatch || !mktBoldMatch || !bridgeBoldMatch) {
271
+ return { dualLens: null, dualLensNote: null };
272
+ }
273
+ const opBoldStart = dlSection.indexOf(opBoldMatch[0]);
274
+ const mktBoldStart = dlSection.indexOf(mktBoldMatch[0]);
275
+ const bridgeBoldStart = dlSection.indexOf(bridgeBoldMatch[0]);
276
+ const opText = dlSection.substring(opBoldStart + opBoldMatch[0].length, mktBoldStart).trim();
277
+ const mktText = dlSection.substring(mktBoldStart + mktBoldMatch[0].length, bridgeBoldStart).trim();
278
+ const bridgeText = dlSection.substring(bridgeBoldStart + bridgeBoldMatch[0].length).trim();
279
+ const operating = parseSharedForm(opBoldMatch[1].trim(), opText);
280
+ const market = parseSharedForm(mktBoldMatch[1].trim(), mktText);
281
+ const bridge = {
282
+ reconciliation: null,
283
+ diagnostic: bridgeText,
284
+ };
285
+ return { dualLens: { operating, market, bridge }, dualLensNote: null };
286
+ }
287
+ return { dualLens: null, dualLensNote: null };
288
+ }
289
+ /** Parse an Operating/Market form from a Standard O/M section (Pattern 1) */
290
+ function parseStandardForm(label, section) {
291
+ // Description = text before the first **bold** field
292
+ const firstBoldIdx = section.search(/\*\*[A-Z]/);
293
+ const description = firstBoldIdx > 0 ? section.substring(0, firstBoldIdx).trim() : "";
294
+ const celSource = extractSubsectionField(section, "CEL Source");
295
+ const atlInclusion = extractSubsectionField(section, "ATL Inclusion(?:[^:]*)?");
296
+ const formula = extractFirstCodeBlock(section);
297
+ const insight = extractSubsectionField(section, "What it tells you");
298
+ return { label, description, celSource, atlInclusion, formula, insight };
299
+ }
300
+ /** Parse an Operating/Market form from a Shared Churn section (Pattern 2) */
301
+ function parseSharedForm(label, section) {
302
+ // Description = text before the first **bold** field
303
+ const firstBoldIdx = section.search(/\*\*[A-Z]/);
304
+ const description = firstBoldIdx > 0 ? section.substring(0, firstBoldIdx).trim() : "";
305
+ const celSource = extractSubsectionField(section, "CEL Source");
306
+ const insight = extractSubsectionField(section, "What it tells you");
307
+ return { label, description, celSource, atlInclusion: "", formula: null, insight };
308
+ }
309
+ /** Parse a Bridge section */
310
+ function parseBridgeSection(section) {
311
+ const reconciliation = extractFirstCodeBlock(section);
312
+ const diagnostic = extractSubsectionField(section, "Diagnostic");
313
+ return { reconciliation, diagnostic };
314
+ }
206
315
  /**
207
316
  * Parse definitions.md into Definition objects.
208
317
  * Each ### heading is a term.