@clavero/mcp-server 2.0.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/package.json +20 -0
- package/src/index.js +262 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clavero/mcp-server",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP server for Clavero — gives AI agents direct access to your product's strategic context",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"clavero": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
+
"zod": "^3.25.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
12
|
+
|
|
13
|
+
const API_URL = process.env.CLAVERO_API_URL || "https://www.clavero.ai";
|
|
14
|
+
const API_KEY = process.env.CLAVERO_API_KEY;
|
|
15
|
+
|
|
16
|
+
async function api(path, options = {}) {
|
|
17
|
+
if (!API_KEY) throw new Error("CLAVERO_API_KEY not set");
|
|
18
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
19
|
+
...options,
|
|
20
|
+
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", ...options.headers },
|
|
21
|
+
});
|
|
22
|
+
const ct = res.headers.get("content-type") || "";
|
|
23
|
+
if (ct.includes("application/json")) {
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
const text = await res.text();
|
|
29
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
34
|
+
const err = (msg) => ({ content: [{ type: "text", text: `Error: ${msg}` }], isError: true });
|
|
35
|
+
|
|
36
|
+
const server = new McpServer({ name: "clavero", version: pkg.version });
|
|
37
|
+
|
|
38
|
+
server.tool("clavero_get_beliefs", "Get all active strategic beliefs with confidence levels and evidence counts.", {}, async () => {
|
|
39
|
+
try {
|
|
40
|
+
const data = await api("/api/v1/beliefs");
|
|
41
|
+
if (!data.beliefs?.length) return ok("No active beliefs.");
|
|
42
|
+
return ok(data.beliefs.map((b) => `- [${b.confidence}] ${b.statement} (${b.evidenceCount || 0} evidence)`).join("\n"));
|
|
43
|
+
} catch (e) { return err(e.message); }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.tool("clavero_check_beliefs", "Check if any beliefs have changed since a given date.", {
|
|
47
|
+
since: z.string().optional().describe("ISO date. Defaults to 24h ago."),
|
|
48
|
+
}, async ({ since }) => {
|
|
49
|
+
const sinceDate = since || new Date(Date.now() - 86400000).toISOString();
|
|
50
|
+
try {
|
|
51
|
+
const data = await api(`/api/v1/beliefs/changes?since=${sinceDate}`);
|
|
52
|
+
if (!data.changes?.length) return ok("No belief changes since " + sinceDate);
|
|
53
|
+
return ok("Belief changes:\n" + data.changes.map((c) => `- [${c.confidence}] ${c.statement}`).join("\n"));
|
|
54
|
+
} catch (e) { return err(e.message); }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
server.tool("clavero_get_context", "Search context entries in the workspace.", {
|
|
58
|
+
query: z.string().optional().describe("Search query"),
|
|
59
|
+
limit: z.number().optional().default(10).describe("Max results"),
|
|
60
|
+
}, async ({ query, limit }) => {
|
|
61
|
+
try {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (query) params.set("q", query);
|
|
64
|
+
if (limit) params.set("limit", String(limit));
|
|
65
|
+
const data = await api(`/api/v1/context?${params}`);
|
|
66
|
+
if (!data.entries?.length) return ok("No context entries found.");
|
|
67
|
+
return ok(data.entries.map((e) => `### ${e.title}${e.source ? ` (${e.source})` : ""}\n${e.content}\n`).join("\n"));
|
|
68
|
+
} catch (e) { return err(e.message); }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.tool(
|
|
72
|
+
"clavero_get_design_principles",
|
|
73
|
+
"DEPRECATED — prefer clavero_get_guiding_principles. This tool now returns a flattened view of the workspace's guiding principles (statement + rationale only). The guiding principles system has categories, severity, and scope — use the newer tool to see them.",
|
|
74
|
+
{},
|
|
75
|
+
async () => {
|
|
76
|
+
try {
|
|
77
|
+
const data = await api("/api/v1/design-principles");
|
|
78
|
+
if (!data.principles?.length) return ok("No principles set.");
|
|
79
|
+
return ok(data.principles.map((p) => `- ${p.statement}${p.rationale ? ` — ${p.rationale}` : ""}`).join("\n"));
|
|
80
|
+
} catch (e) { return err(e.message); }
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
server.tool(
|
|
85
|
+
"clavero_get_guiding_principles",
|
|
86
|
+
"Get the workspace's guiding principles — rules that govern execution. Three categories: directional (what to build toward), boundary (what NOT to build — the most important category), and standard (how to build). Each principle has a severity (hard/soft/advisory). ALWAYS respect these when making implementation decisions.",
|
|
87
|
+
{
|
|
88
|
+
themeId: z.string().optional().describe("Optional theme ID for theme-scoped principles alongside global"),
|
|
89
|
+
},
|
|
90
|
+
async ({ themeId }) => {
|
|
91
|
+
try {
|
|
92
|
+
const url = themeId
|
|
93
|
+
? `/api/v1/guiding-principles?themeId=${encodeURIComponent(themeId)}`
|
|
94
|
+
: `/api/v1/guiding-principles`;
|
|
95
|
+
const data = await api(url);
|
|
96
|
+
|
|
97
|
+
const formatCategory = (principles, icon, label) => {
|
|
98
|
+
if (!principles?.length) return "";
|
|
99
|
+
const lines = principles.map((p) => {
|
|
100
|
+
let line = `- [${p.severity}] ${p.statement}`;
|
|
101
|
+
if (p.explanation) line += `\n ${p.explanation}`;
|
|
102
|
+
if (p.violationExample) line += `\n ⚠ Violation: ${p.violationExample}`;
|
|
103
|
+
if (p.test) line += `\n ? Test: ${p.test}`;
|
|
104
|
+
return line;
|
|
105
|
+
}).join("\n");
|
|
106
|
+
return `${icon} ${label}:\n${lines}\n`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const globalSections = [
|
|
110
|
+
formatCategory(data.global?.directional, "→", "Directional"),
|
|
111
|
+
formatCategory(data.global?.boundary, "⛔", "Boundaries"),
|
|
112
|
+
formatCategory(data.global?.standard, "⚙", "Standards"),
|
|
113
|
+
].filter(Boolean).join("\n");
|
|
114
|
+
|
|
115
|
+
const themeSections = themeId
|
|
116
|
+
? [
|
|
117
|
+
formatCategory(data.themeScoped?.directional, "→", "Directional"),
|
|
118
|
+
formatCategory(data.themeScoped?.boundary, "⛔", "Boundaries"),
|
|
119
|
+
formatCategory(data.themeScoped?.standard, "⚙", "Standards"),
|
|
120
|
+
].filter(Boolean).join("\n")
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
if (!globalSections && !themeSections) return ok("No guiding principles set.");
|
|
124
|
+
|
|
125
|
+
let text = "GUIDING PRINCIPLES\n\nRules governing execution. Pay special attention to boundary principles with 'hard' severity — violations must be reviewed before shipping.\n\n";
|
|
126
|
+
if (globalSections) text += globalSections;
|
|
127
|
+
if (themeSections) text += `\nArea-specific principles:\n${themeSections}`;
|
|
128
|
+
return ok(text);
|
|
129
|
+
} catch (e) { return err(e.message); }
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
134
|
+
"clavero_get_investment_areas",
|
|
135
|
+
"Get active investment areas. These are the PM's current priorities — where the team should invest energy. Each area includes: why, beliefs, success signals, direction, boundaries, and anti-patterns. Use this to understand what to build and how to build it correctly.",
|
|
136
|
+
{
|
|
137
|
+
priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by priority"),
|
|
138
|
+
status: z.enum(["active", "paused", "completed", "archived"]).optional().describe("Filter by status (default: active)"),
|
|
139
|
+
},
|
|
140
|
+
async ({ priority, status }) => {
|
|
141
|
+
try {
|
|
142
|
+
const params = new URLSearchParams();
|
|
143
|
+
if (priority) params.set("priority", priority);
|
|
144
|
+
if (status) params.set("status", status);
|
|
145
|
+
const data = await api(`/api/v1/investment-areas?${params}`);
|
|
146
|
+
|
|
147
|
+
if (!data.areas?.length) return ok("No investment areas found.");
|
|
148
|
+
|
|
149
|
+
const formatted = data.areas.map((area) => {
|
|
150
|
+
let text = `\n${"━".repeat(60)}\n`;
|
|
151
|
+
text += `${area.priority.toUpperCase()} | ${area.name} | ${area.timeframe} | ${area.status}\n`;
|
|
152
|
+
text += `${"━".repeat(60)}\n\n`;
|
|
153
|
+
if (area.context) text += `WHY:\n${area.context}\n\n`;
|
|
154
|
+
if (area.beliefs?.length) {
|
|
155
|
+
text += `BELIEFS:\n${area.beliefs.map((b) => ` [${b.confidence}] ${b.statement}`).join("\n")}\n\n`;
|
|
156
|
+
}
|
|
157
|
+
if (area.successSignals?.length) {
|
|
158
|
+
text += `SUCCESS SIGNALS:\n${area.successSignals.map((s) => {
|
|
159
|
+
const progressIcon = s.progress === "improving" ? "↑" : s.progress === "declining" ? "↓" : s.progress === "steady" ? "→" : "?";
|
|
160
|
+
let line = ` ${progressIcon} ${s.signal}`;
|
|
161
|
+
if (s.metric) line += ` (metric: ${s.metric})`;
|
|
162
|
+
if (s.target) line += ` (target: ${s.target})`;
|
|
163
|
+
if (s.progress) line += ` [${s.progress}]`;
|
|
164
|
+
return line;
|
|
165
|
+
}).join("\n")}\n\n`;
|
|
166
|
+
}
|
|
167
|
+
if (area.directionalPrinciples?.length) {
|
|
168
|
+
text += `DIRECTION:\n${area.directionalPrinciples.map((p) => ` → [${p.severity}] ${p.statement}`).join("\n")}\n\n`;
|
|
169
|
+
}
|
|
170
|
+
if (area.boundaryPrinciples?.length) {
|
|
171
|
+
text += `BOUNDARIES:\n${area.boundaryPrinciples.map((p) => {
|
|
172
|
+
let line = ` ⛔ [${p.severity}] ${p.statement}`;
|
|
173
|
+
if (p.violationExample) line += `\n Violation: ${p.violationExample}`;
|
|
174
|
+
return line;
|
|
175
|
+
}).join("\n")}\n\n`;
|
|
176
|
+
}
|
|
177
|
+
if (area.antiPatterns?.length) {
|
|
178
|
+
text += `ANTI-PATTERNS:\n${area.antiPatterns.map((a) => ` ✗ ${a.description}`).join("\n")}\n\n`;
|
|
179
|
+
}
|
|
180
|
+
if (area.standardPrinciples?.length) {
|
|
181
|
+
text += `STANDARDS:\n${area.standardPrinciples.map((p) => ` ⚙ [${p.severity}] ${p.statement}`).join("\n")}\n`;
|
|
182
|
+
}
|
|
183
|
+
return text;
|
|
184
|
+
}).join("\n");
|
|
185
|
+
|
|
186
|
+
return ok(`ACTIVE INVESTMENT AREAS\n\nBuild within these areas, respecting direction and boundaries.\n${formatted}`);
|
|
187
|
+
} catch (e) { return err(e.message); }
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
server.tool(
|
|
192
|
+
"clavero_check_compliance",
|
|
193
|
+
"Check whether a proposed feature or change complies with the workspace's guiding principles and aligns with active investment areas. Returns violations (hard breaches), concerns (soft breaches), aligned principles, area alignment, and suggested alternatives. ALWAYS call this before implementing any non-trivial feature to catch strategically-wrong work before it ships.",
|
|
194
|
+
{
|
|
195
|
+
proposal: z.string().describe("Description of what you want to build or change — free text, a spec draft, or a sentence."),
|
|
196
|
+
themeId: z.string().optional().describe("Investment area ID if this proposal relates to a specific area"),
|
|
197
|
+
},
|
|
198
|
+
async ({ proposal, themeId }) => {
|
|
199
|
+
try {
|
|
200
|
+
const data = await api("/api/v1/compliance-check", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: JSON.stringify({ proposal, themeId }),
|
|
203
|
+
});
|
|
204
|
+
return ok(data.assessment || "Compliance check returned no assessment.");
|
|
205
|
+
} catch (e) { return err(e.message); }
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
server.tool("clavero_capture_context", "Save a piece of strategic context to Clavero.", {
|
|
210
|
+
title: z.string().describe("Short descriptive title"),
|
|
211
|
+
content: z.string().describe("The insight or signal to capture"),
|
|
212
|
+
source: z.string().optional().default("Claude conversation").describe("Source"),
|
|
213
|
+
}, async ({ title, content, source }) => {
|
|
214
|
+
try {
|
|
215
|
+
const data = await api("/api/v1/context", { method: "POST", body: JSON.stringify({ title, content, source }) });
|
|
216
|
+
let response = `Captured: "${data.title}"`;
|
|
217
|
+
if (data.beliefSuggestions?.length) {
|
|
218
|
+
response += `\n${data.beliefSuggestions.length} belief link${data.beliefSuggestions.length > 1 ? "s" : ""} suggested:`;
|
|
219
|
+
data.beliefSuggestions.forEach((s) => {
|
|
220
|
+
response += `\n - ${s.direction === "supports" ? "↑" : s.direction === "challenges" ? "↓" : "→"} ${s.beliefStatement} (${s.strength})`;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return ok(response);
|
|
224
|
+
} catch (e) { return err(e.message); }
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
server.tool("clavero_generate_spec", "Generate a detailed implementation spec grounded in strategic context.", {
|
|
228
|
+
decisionId: z.string().optional().describe("Decision ID from the decision log"),
|
|
229
|
+
description: z.string().optional().describe("Free-text description of what to build"),
|
|
230
|
+
}, async ({ decisionId, description }) => {
|
|
231
|
+
if (!decisionId && !description) return err("Provide either decisionId or description.");
|
|
232
|
+
try {
|
|
233
|
+
const data = await api("/api/v1/generate-spec", { method: "POST", body: JSON.stringify({ decisionId, description }) });
|
|
234
|
+
return ok(data.spec || "Spec generated.");
|
|
235
|
+
} catch (e) { return err(e.message); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
server.tool("clavero_list_decisions", "List recent decisions from the decision log.", {
|
|
239
|
+
limit: z.number().optional().default(10).describe("Max results"),
|
|
240
|
+
}, async ({ limit }) => {
|
|
241
|
+
try {
|
|
242
|
+
const data = await api(`/api/v1/decisions?limit=${limit}`);
|
|
243
|
+
if (!data.decisions?.length) return ok("No decisions recorded.");
|
|
244
|
+
return ok(data.decisions.map((d) => `- [${d.reflectionStatus}] ${d.title}\n Reasoning: ${(d.reasoning || "").slice(0, 100)}...`).join("\n\n"));
|
|
245
|
+
} catch (e) { return err(e.message); }
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
server.tool("clavero_log_decision", "Log a decision with reasoning.", {
|
|
249
|
+
title: z.string().describe("What was decided"),
|
|
250
|
+
reasoning: z.string().describe("Why"),
|
|
251
|
+
alternatives: z.string().optional().describe("Alternatives considered"),
|
|
252
|
+
assumptions: z.string().optional().describe("Key assumptions"),
|
|
253
|
+
}, async ({ title, reasoning, alternatives, assumptions }) => {
|
|
254
|
+
try {
|
|
255
|
+
await api("/api/v1/decisions", { method: "POST", body: JSON.stringify({ title, reasoning, alternatives, assumptions }) });
|
|
256
|
+
return ok(`Decision logged: "${title}". Reflection due in 30 days.`);
|
|
257
|
+
} catch (e) { return err(e.message); }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const transport = new StdioServerTransport();
|
|
261
|
+
await server.connect(transport);
|
|
262
|
+
console.error(`Clavero MCP server v${pkg.version} running`);
|