gasp-standard-mcp 1.0.0 → 1.2.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/README.md +6 -3
- package/build/export-data.js +7 -1
- package/build/index.js +85 -4
- package/build/loader.d.ts +1 -1
- package/build/loader.js +80 -2
- package/build/parser.js +2 -0
- package/build/standard-data.json +1 -1
- package/build/standard-version.json +1 -1
- package/build/types.d.ts +19 -0
- package/package.json +5 -8
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
|
-
| `
|
|
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
|
-
-
|
|
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
|
package/build/export-data.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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 */
|