gasp-standard-mcp 1.0.1 → 1.2.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
@@ -37,13 +37,15 @@ Restart your MCP client after adding the config.
37
37
  | `list_metrics` | List all metrics, optionally filtered by department |
38
38
  | `get_formula` | Get formula(s) for a metric |
39
39
  | `get_relationships` | Upstream and downstream metric relationships |
40
- | `search` | Full-text search across all metrics and definitions |
40
+ | `get_data_requirements` | Data requirements for implementing a metric: fields, types, grain, source, and example SQL |
41
+ | `search` | Full-text search across metrics, definitions, and data requirements |
41
42
 
42
43
  ## Data
43
44
 
44
45
  - 299 metrics across 13 departments
45
46
  - 79 glossary definitions
46
- - 21 relationship nodes, 25 edges
47
+ - 22 relationship nodes, 34 edges
48
+ - 75 data requirements with field types, grain, and example SQL
47
49
  - Sources: the canonical [GASP Standard](https://gaspwiki.com) files
48
50
 
49
51
  ## Examples
@@ -54,6 +56,7 @@ Once installed, ask your AI assistant:
54
56
  - "How do I calculate CAC Payback?"
55
57
  - "What drives ARR?"
56
58
  - "List all marketing metrics"
59
+ - "What data do I need to implement ARR tracking?"
57
60
  - "Search for churn"
58
61
 
59
62
  ## License
@@ -15,6 +15,7 @@ const version = {
15
15
  definitionCount: data.definitions.length,
16
16
  nodeCount: data.nodes.length,
17
17
  edgeCount: data.edges.length,
18
+ dataRequirementCount: data.dataRequirements.length,
18
19
  buildDate: new Date().toISOString().slice(0, 10),
19
20
  };
20
21
  const versionJson = JSON.stringify(version);
@@ -27,6 +28,11 @@ writeFileSync(join(webOutDir, "standard-version.json"), versionJson);
27
28
  const buildDir = new URL(".", import.meta.url).pathname;
28
29
  writeFileSync(join(buildDir, "standard-data.json"), json);
29
30
  writeFileSync(join(buildDir, "standard-version.json"), versionJson);
30
- console.log(`Exported ${data.metrics.length} metrics, ${data.definitions.length} definitions, ${data.nodes.length} nodes, ${data.edges.length} edges (${version.buildDate})`);
31
+ // 3. Write to website/src/data/ → importable by React components
32
+ const srcDataDir = join(new URL(".", import.meta.url).pathname, "..", "..", "website", "src", "data");
33
+ mkdirSync(srcDataDir, { recursive: true });
34
+ writeFileSync(join(srcDataDir, "standard.json"), json);
35
+ console.log(`Exported ${data.metrics.length} metrics, ${data.definitions.length} definitions, ${data.nodes.length} nodes, ${data.edges.length} edges, ${data.dataRequirements.length} data requirements (${version.buildDate})`);
31
36
  console.log(` → website/public/api/standard.json + standard-version.json`);
32
37
  console.log(` → mcp-server/build/standard-data.json + standard-version.json`);
38
+ console.log(` → website/src/data/standard.json`);
package/build/index.js CHANGED
@@ -59,10 +59,15 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
59
59
  };
60
60
  }
61
61
  // Build response with branding swap
62
+ const signalLabel = metric.signalType.charAt(0).toUpperCase() + metric.signalType.slice(1);
63
+ const signalLine = metric.signalTypeSecondary
64
+ ? `**Signal Type:** ${signalLabel} (+ ${metric.signalTypeSecondary.charAt(0).toUpperCase() + metric.signalTypeSecondary.slice(1)})`
65
+ : `**Signal Type:** ${signalLabel}`;
62
66
  const parts = [
63
67
  `# ${metric.name}`,
64
68
  `**Department:** ${metric.department}`,
65
69
  `**Category:** ${metric.category}`,
70
+ signalLine,
66
71
  ];
67
72
  if (metric.definition) {
68
73
  parts.push(`\n**Definition:** ${metric.definition}`);
@@ -94,16 +99,23 @@ server.tool("lookup_metric", "Look up a GASP Standard metric by name or abbrevia
94
99
  return { content: [{ type: "text", text }] };
95
100
  });
96
101
  // --- Tool 2: list_metrics ---
97
- server.tool("list_metrics", "List all GASP Standard metrics, optionally filtered by department. Returns metric names grouped by category.", {
102
+ server.tool("list_metrics", "List all GASP Standard metrics, optionally filtered by department and/or signal type. Returns metric names grouped by category.", {
98
103
  department: z
99
104
  .string()
100
105
  .optional()
101
106
  .describe("Filter by department name (e.g. 'sales', 'marketing', 'core'). Case-insensitive. Omit to list all."),
102
- }, async ({ department }) => {
107
+ signalType: z
108
+ .enum(["outcome", "leading", "efficiency", "operational"])
109
+ .optional()
110
+ .describe("Filter by signal type. Returns metrics where primary or secondary type matches."),
111
+ }, async ({ department, signalType }) => {
103
112
  let filtered = data.metrics;
104
113
  if (department) {
105
114
  const q = department.toLowerCase();
106
- filtered = data.metrics.filter((m) => m.department.toLowerCase().includes(q));
115
+ filtered = filtered.filter((m) => m.department.toLowerCase().includes(q));
116
+ }
117
+ if (signalType) {
118
+ filtered = filtered.filter((m) => m.signalType === signalType || m.signalTypeSecondary === signalType);
107
119
  }
108
120
  if (filtered.length === 0) {
109
121
  const departments = [
@@ -218,7 +230,76 @@ server.tool("get_relationships", "Get the upstream (what drives it) and downstre
218
230
  }
219
231
  return { content: [{ type: "text", text: parts.join("\n") }] };
220
232
  });
221
- // --- Tool 5: search ---
233
+ // --- Tool 5: get_data_requirements ---
234
+ server.tool("get_data_requirements", "Get the data requirements for implementing a GASP Standard metric or department dashboard. Returns required fields with types, reporting grain, source system, and example SQL query.", {
235
+ metric: z
236
+ .string()
237
+ .optional()
238
+ .describe("Metric name to search for (e.g. 'ARR', 'Pipeline', 'CSAT')"),
239
+ department: z
240
+ .string()
241
+ .optional()
242
+ .describe("Department name (e.g. 'Sales', 'Engineering', 'Executive'). Omit to search all."),
243
+ }, async ({ metric, department }) => {
244
+ let filtered = data.dataRequirements || [];
245
+ if (department) {
246
+ const q = department.toLowerCase();
247
+ filtered = filtered.filter((dr) => dr.department.toLowerCase().includes(q));
248
+ }
249
+ if (metric) {
250
+ const q = metric.toLowerCase();
251
+ filtered = filtered.filter((dr) => dr.metric.toLowerCase() === q ||
252
+ dr.metric.toLowerCase().includes(q) ||
253
+ dr.fields.some((f) => f.toLowerCase().includes(q)));
254
+ }
255
+ if (filtered.length === 0) {
256
+ const depts = [
257
+ ...new Set((data.dataRequirements || []).map((dr) => dr.department)),
258
+ ].sort();
259
+ return {
260
+ content: [
261
+ {
262
+ type: "text",
263
+ text: `No data requirements found${metric ? ` for metric "${metric}"` : ""}${department ? ` in department "${department}"` : ""}.\n\nAvailable departments: ${depts.join(", ")}`,
264
+ },
265
+ ],
266
+ };
267
+ }
268
+ const parts = [
269
+ `# Data Requirements (${filtered.length} entries)\n`,
270
+ ];
271
+ // Group by department
272
+ const grouped = new Map();
273
+ for (const dr of filtered) {
274
+ if (!grouped.has(dr.department))
275
+ grouped.set(dr.department, []);
276
+ grouped.get(dr.department).push(dr);
277
+ }
278
+ for (const [dept, drs] of grouped) {
279
+ parts.push(`## ${dept}\n`);
280
+ for (const dr of drs) {
281
+ parts.push(`### ${dr.metric}`);
282
+ parts.push(`- **Source:** ${dr.source}`);
283
+ parts.push(`- **Grain:** ${dr.grain}`);
284
+ parts.push(`- **Frequency:** ${dr.frequency}`);
285
+ parts.push(`- **Description:** ${dr.description}`);
286
+ parts.push("");
287
+ // Field table
288
+ parts.push("| Field | Type |");
289
+ parts.push("|-------|------|");
290
+ for (const field of dr.fields) {
291
+ const fieldType = dr.fieldTypes?.[field] || "unknown";
292
+ parts.push(`| \`${field}\` | ${fieldType} |`);
293
+ }
294
+ parts.push("");
295
+ // SQL
296
+ parts.push("**Example SQL:**");
297
+ parts.push("```sql\n" + dr.exampleQuery + "\n```\n");
298
+ }
299
+ }
300
+ return { content: [{ type: "text", text: parts.join("\n") }] };
301
+ });
302
+ // --- Tool 6: search ---
222
303
  server.tool("search", "Search across all GASP Standard metrics and definitions. Returns top 10 matches ranked by relevance.", { query: z.string().describe("Search query (e.g. 'churn', 'retention', 'pipeline coverage')") }, async ({ query: q }) => {
223
304
  const results = search(data, q, 10);
224
305
  if (results.length === 0) {
package/build/loader.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare function loadStandard(): StandardData;
10
10
  * Returns top N matches ranked by relevance.
11
11
  */
12
12
  export declare function search(data: StandardData, query: string, limit?: number): Array<{
13
- type: "metric" | "definition";
13
+ type: "metric" | "definition" | "data_requirement";
14
14
  name: string;
15
15
  snippet: string;
16
16
  department?: string;
package/build/loader.js CHANGED
@@ -42,7 +42,62 @@ export function loadStandard() {
42
42
  const relData = JSON.parse(readFileSync(relPath, "utf-8"));
43
43
  const nodes = relData.nodes;
44
44
  const edges = relData.edges;
45
- return { metrics, definitions, nodes, edges };
45
+ // Load data requirements from dashboard
46
+ const dashPath = join(root, "website", "src", "data", "dashboard-mock.json");
47
+ const dashData = JSON.parse(readFileSync(dashPath, "utf-8"));
48
+ const dataRequirements = [];
49
+ for (const dr of dashData.dataRequirements || []) {
50
+ dataRequirements.push({ ...dr, department: "Executive" });
51
+ }
52
+ for (const [, dept] of Object.entries(dashData.departments || {})) {
53
+ const d = dept;
54
+ for (const dr of d.dataRequirements || []) {
55
+ dataRequirements.push({ ...dr, department: d.name });
56
+ }
57
+ }
58
+ // Load taxonomy classifications
59
+ const taxonomyPath = join(standardDir, "taxonomy.json");
60
+ let taxonomy = { classifications: {} };
61
+ try {
62
+ taxonomy = JSON.parse(readFileSync(taxonomyPath, "utf-8"));
63
+ }
64
+ catch {
65
+ // taxonomy.json not yet created — default to operational
66
+ }
67
+ // Merge signal types into metrics
68
+ for (const m of metrics) {
69
+ const deptSlug = m.department.toLowerCase().replace(/\s+/g, "-");
70
+ const key = `${deptSlug}::${m.slug}`;
71
+ const entry = taxonomy.classifications[key];
72
+ if (entry) {
73
+ m.signalType = entry.primary;
74
+ m.signalTypeSecondary = entry.secondary || null;
75
+ }
76
+ else {
77
+ // Cross-reference stubs: look up canonical metric's classification
78
+ const seeMatch = m.rawMarkdown.match(/See \[([^\]]+)\]\(([^)]+)\)/);
79
+ if (seeMatch) {
80
+ const link = seeMatch[2];
81
+ const anchor = link.includes("#") ? link.split("#")[1] : null;
82
+ if (anchor) {
83
+ // Find canonical metric's key
84
+ for (const [canonKey, canonEntry] of Object.entries(taxonomy.classifications)) {
85
+ if (canonKey.endsWith(`::${anchor}`)) {
86
+ m.signalType = canonEntry.primary;
87
+ m.signalTypeSecondary = canonEntry.secondary || null;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ // Fallback if still unset
94
+ if (!m.signalType) {
95
+ m.signalType = "operational";
96
+ m.signalTypeSecondary = null;
97
+ }
98
+ }
99
+ }
100
+ return { metrics, definitions, nodes, edges, dataRequirements };
46
101
  }
47
102
  /**
48
103
  * Search metrics + definitions for a query string.
@@ -54,10 +109,11 @@ export function search(data, query, limit = 10) {
54
109
  for (const m of data.metrics) {
55
110
  const score = scoreMetric(m, q);
56
111
  if (score > 0) {
112
+ const signalLabel = m.signalType.charAt(0).toUpperCase() + m.signalType.slice(1);
57
113
  results.push({
58
114
  type: "metric",
59
115
  name: m.name,
60
- snippet: m.definition || m.insight || "",
116
+ snippet: `[${signalLabel}] ${m.definition || m.insight || ""}`,
61
117
  department: m.department,
62
118
  score,
63
119
  });
@@ -74,6 +130,28 @@ export function search(data, query, limit = 10) {
74
130
  });
75
131
  }
76
132
  }
133
+ for (const dr of data.dataRequirements || []) {
134
+ let score = 0;
135
+ if (dr.metric.toLowerCase() === q)
136
+ score += 70;
137
+ else if (dr.metric.toLowerCase().includes(q))
138
+ score += 35;
139
+ if (dr.description.toLowerCase().includes(q))
140
+ score += 10;
141
+ if (dr.fields.some((f) => f.toLowerCase().includes(q)))
142
+ score += 15;
143
+ if (dr.department.toLowerCase().includes(q))
144
+ score += 5;
145
+ if (score > 0) {
146
+ results.push({
147
+ type: "data_requirement",
148
+ name: dr.metric,
149
+ snippet: `[${dr.department}] ${dr.description} (grain: ${dr.grain})`,
150
+ department: dr.department,
151
+ score,
152
+ });
153
+ }
154
+ }
77
155
  results.sort((a, b) => b.score - a.score);
78
156
  return results.slice(0, limit).map(({ score: _, ...rest }) => rest);
79
157
  }
package/build/parser.js CHANGED
@@ -145,6 +145,8 @@ function parseSectionFields(name, slug, raw, department, category, aliases) {
145
145
  category,
146
146
  aliases,
147
147
  rawMarkdown: `### ${name}\n\n${raw}`,
148
+ signalType: "operational",
149
+ signalTypeSecondary: null,
148
150
  };
149
151
  }
150
152
  /** Extract a **FieldName:** value from markdown */