@clarxai/mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/dist/index.js +648 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @clarxai/mcp
2
+
3
+ Clarx MCP server — score agent manifests (`CLAUDE.md` / `AGENTS.md`), validate `clarx-manifest.json`, and generate Clarx CI workflows directly from your coding agent (Cursor, Claude Code, Claude Desktop, Windsurf, …).
4
+
5
+ **No account required.** The core tools run entirely on your machine with the same rules as the [Clarx manifest studio](https://clarx.ai).
6
+
7
+ ## Setup
8
+
9
+ Add to your MCP client config (e.g. `.cursor/mcp.json`, `claude_desktop_config.json`):
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "clarx": {
15
+ "command": "npx",
16
+ "args": ["-y", "@clarxai/mcp@latest"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ Claude Code:
23
+
24
+ ```bash
25
+ claude mcp add clarx -- npx -y @clarxai/mcp@latest
26
+ ```
27
+
28
+ ## Tools
29
+
30
+ | Tool | What it does | Requires |
31
+ |------|--------------|----------|
32
+ | `analyze_manifest` | Score a CLAUDE.md / AGENTS.md (0–100, pillar scores, findings with line numbers) | — |
33
+ | `analyze_clarx_manifest` | Validate `clarx-manifest.json` against the engine schema | — |
34
+ | `get_ci_workflow` | Generate a GitHub Actions workflow that gates PRs on the Clarx score | — |
35
+ | `suggest_manifest_fix` | AI-write one manifest section to close a finding | `ANTHROPIC_API_KEY` |
36
+ | `generate_manifest_draft` | AI-draft a full manifest from your README | `ANTHROPIC_API_KEY` |
37
+
38
+ The AI tools are bring-your-own-key: set `ANTHROPIC_API_KEY` in the server `env` and they register automatically; without it they stay out of your agent's context.
39
+
40
+ `analyze_manifest` scores are **manifest quality estimates** — the same rules the manifest studio uses. They are not comparable to Clarx repo AI-readiness scores, which come from a full engine scan.
41
+
42
+ ## Hosted mode (coming soon)
43
+
44
+ Setting `CLARX_MCP_TOKEN` will unlock hosted tools — scan results, findings, and remediation prompts from your Clarx Cloud dashboard. Not available in this version.
package/dist/index.js ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/local-tools.ts
8
+ import { readFile } from "fs/promises";
9
+ import { resolve } from "path";
10
+ import { z } from "zod";
11
+
12
+ // ../../packages/mcp-core/src/manifest-analyzer.ts
13
+ var EXPECTED_SECTIONS = [
14
+ { key: "overview", label: "Project overview", pillar: "D", keywords: /(overview|introduction|about|what is|purpose)/i, minWords: 25, hint: "1-3 sentences. What is the product, who uses it, what problem does it solve." },
15
+ { key: "architecture", label: "Architecture & module map", pillar: "B", keywords: /(architecture|design|system|module|services?|deployable|topology)/i, minWords: 40, needsStructure: true, hint: "How the system is split. Name the deployable units and where each lives in the tree." },
16
+ { key: "build", label: "Build / install", pillar: "O", keywords: /(build|install|setup|getting started|run\b|dev server|develop)/i, minWords: 6, needsCode: true, hint: "The commands an agent runs to get a working environment. Show in a code block." },
17
+ { key: "testing", label: "Testing & verification", pillar: "O", keywords: /(test|verify|qa|spec\b|coverage)/i, minWords: 10, hint: "How to run tests + where they live. Mention the test runner." },
18
+ { key: "conventions", label: "Code conventions", pillar: "E", keywords: /(convention|style|standard|guideline|format|lint|rules?:)/i, minWords: 20, hint: "Naming, exports, imports, comments. The 'house rules' an agent should not violate." },
19
+ { key: "directory", label: "Directory structure", pillar: "D", keywords: /(director(y|ies)|file (tree|layout)|project structure|repo (layout|map))/i, minWords: 8, needsCode: true, hint: "A code-block tree of top-level folders with a 1-line gloss for each." },
20
+ { key: "entry", label: "Entry points / key files", pillar: "D", keywords: /(entry point|main file|where to start|key files?|important files?)/i, minWords: 10, hint: "Where to begin reading. Link 3-6 files an agent should open first." },
21
+ { key: "glossary", label: "Domain glossary", pillar: "E", keywords: /(glossary|terms?|vocabular|definitions|domain language)/i, minWords: 12, hint: "Domain words and their precise meaning in this codebase. Prevents misinterpretation." },
22
+ { key: "tasks", label: "Common tasks / recipes", pillar: "E", keywords: /(common tasks|how to|recipes?|workflow|playbook|how do i)/i, minWords: 15, hint: "Step-by-step for the 3-5 things an agent will be asked to do most often." },
23
+ { key: "deployment", label: "Deployment / ops", pillar: "O", keywords: /(deploy|production|release|ship|ci\/cd|pipeline|ops)/i, minWords: 10, hint: "How code reaches production. Branch model, deploy commands, environment names." },
24
+ { key: "apis", label: "External APIs & integrations", pillar: "B", keywords: /(api integrations?|external (service|api)|third.party|integrations?:)/i, minWords: 10, hint: "Outbound dependencies \u2014 names, why they're used, where credentials are configured." }
25
+ ];
26
+ var PILLARS = [
27
+ { key: "D", name: "Discoverability", code: "D-rules" },
28
+ { key: "B", name: "Boundary clarity", code: "B-rules" },
29
+ { key: "C", name: "Context efficiency", code: "C-rules" },
30
+ { key: "O", name: "Operational guidance", code: "O-rules" },
31
+ { key: "E", name: "Edit safety", code: "E-rules" }
32
+ ];
33
+ function parseHeaders(content) {
34
+ const lines = content.split("\n");
35
+ const headers = [];
36
+ let inCode = false;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ if (/^```/.test(line.trim())) {
40
+ inCode = !inCode;
41
+ continue;
42
+ }
43
+ if (inCode) continue;
44
+ const m = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
45
+ if (m) headers.push({ line: i + 1, level: m[1].length, title: m[2].trim() });
46
+ }
47
+ return headers;
48
+ }
49
+ function buildSections(content, headers) {
50
+ const lines = content.split("\n");
51
+ return headers.map((h, idx) => {
52
+ let endLine = lines.length;
53
+ for (let j = idx + 1; j < headers.length; j++) {
54
+ if (headers[j].level <= h.level) {
55
+ endLine = headers[j].line - 1;
56
+ break;
57
+ }
58
+ }
59
+ const body = lines.slice(h.line, endLine).join("\n");
60
+ return {
61
+ ...h,
62
+ endLine,
63
+ body,
64
+ words: body.replace(/```[\s\S]*?```/g, "").split(/\s+/).filter(Boolean).length,
65
+ hasCode: /```[\s\S]+?```/.test(body),
66
+ hasList: /^\s*[-*+]\s/m.test(body) || /^\s*\d+\.\s/m.test(body),
67
+ hasFileRef: /`[^`]*\.[a-zA-Z]{1,5}`|`[^`]*\/[^`]+`/.test(body)
68
+ };
69
+ });
70
+ }
71
+ function matchSection(sections, spec) {
72
+ for (const s of sections) if (spec.keywords.test(s.title)) return s;
73
+ for (const s of sections) {
74
+ if (spec.keywords.test(s.body)) return { ...s, matchedInBody: true };
75
+ }
76
+ return null;
77
+ }
78
+ function gradeSection(spec, match) {
79
+ if (!match) return { present: false, score: 0 };
80
+ let score = 1;
81
+ if (match.words < spec.minWords) score -= 0.4;
82
+ if (spec.needsCode && !match.hasCode) score -= 0.35;
83
+ if (spec.needsStructure && !match.hasList && !match.hasCode) score -= 0.25;
84
+ if (match.matchedInBody) score -= 0.3;
85
+ score = Math.max(0, Math.min(1, score));
86
+ const quality = score >= 0.8 ? "good" : score >= 0.45 ? "partial" : "weak";
87
+ return { present: true, score, quality, match };
88
+ }
89
+ function computePillarScores(checklist, content, headers) {
90
+ const lines = content.split("\n").length;
91
+ const byPillar = {};
92
+ for (const p of PILLARS) byPillar[p.key] = { num: 0, den: 0 };
93
+ for (const spec of EXPECTED_SECTIONS) {
94
+ const c = checklist[spec.key];
95
+ byPillar[spec.pillar].den += 1;
96
+ byPillar[spec.pillar].num += c.present ? c.score : 0;
97
+ }
98
+ let contextScore = 1;
99
+ if (lines < 40) contextScore = 0.45;
100
+ else if (lines < 90) contextScore = 0.7;
101
+ else if (lines > 700) contextScore = 0.45;
102
+ else if (lines > 500) contextScore = 0.7;
103
+ byPillar.C.num += contextScore;
104
+ byPillar.C.den += 1;
105
+ const hasH3 = headers.some((h) => h.level === 3);
106
+ if (hasH3) {
107
+ byPillar.D.num += 0.15;
108
+ byPillar.D.den += 0.15;
109
+ }
110
+ const out = {};
111
+ for (const p of PILLARS) {
112
+ const { num, den } = byPillar[p.key];
113
+ out[p.key] = den === 0 ? 0 : Math.round(num / den * 100);
114
+ }
115
+ return out;
116
+ }
117
+ var RULE_ID_MAP = {
118
+ overview: "D2",
119
+ architecture: "B1",
120
+ build: "O1",
121
+ testing: "O3",
122
+ conventions: "E1",
123
+ directory: "D5",
124
+ entry: "D3",
125
+ glossary: "E4",
126
+ tasks: "E2",
127
+ deployment: "O5",
128
+ apis: "B6"
129
+ };
130
+ function buildFindings(content, headers, sections, checklist) {
131
+ const findings = [];
132
+ let id = 1;
133
+ const ruleId = (pillar, key) => RULE_ID_MAP[key] ?? `${pillar}9`;
134
+ for (const spec of EXPECTED_SECTIONS) {
135
+ const c = checklist[spec.key];
136
+ if (!c.present) {
137
+ findings.push({ id: `F${id++}`, rule: ruleId(spec.pillar, spec.key), pillar: spec.pillar, severity: spec.key === "overview" || spec.key === "build" || spec.key === "architecture" ? "warn" : "info", line: 1, title: `Missing section: ${spec.label}`, desc: spec.hint, sectionKey: spec.key, actionable: true });
138
+ continue;
139
+ }
140
+ if (c.match?.matchedInBody) {
141
+ findings.push({ id: `F${id++}`, rule: ruleId(spec.pillar, spec.key), pillar: spec.pillar, severity: "info", line: c.match.line, title: `${spec.label} mentioned but not headlined`, desc: `Lift this content into its own \`## ${spec.label}\` section so agents can jump to it.`, sectionKey: spec.key });
142
+ continue;
143
+ }
144
+ if (c.match && c.match.words < spec.minWords) {
145
+ findings.push({ id: `F${id++}`, rule: ruleId(spec.pillar, spec.key), pillar: spec.pillar, severity: "warn", line: c.match.line, title: `${spec.label} is thin (${c.match.words} words)`, desc: `Expand to at least ${spec.minWords} words. ${spec.hint}`, sectionKey: spec.key });
146
+ }
147
+ if (spec.needsCode && c.match && !c.match.hasCode) {
148
+ findings.push({ id: `F${id++}`, rule: ruleId(spec.pillar, spec.key), pillar: spec.pillar, severity: "warn", line: c.match.line, title: `${spec.label} has no code block`, desc: "Include a runnable snippet (` ```bash \u2026 ``` `) so an agent can copy the command.", sectionKey: spec.key });
149
+ }
150
+ }
151
+ const lines = content.split("\n");
152
+ const lineCount = lines.length;
153
+ if (lineCount < 60) findings.push({ id: `F${id++}`, rule: "C3", pillar: "C", severity: "warn", line: 1, title: "Manifest is very short", desc: `Only ${lineCount} lines. An agent will guess at conventions you haven't stated.` });
154
+ else if (lineCount > 600) findings.push({ id: `F${id++}`, rule: "C1", pillar: "C", severity: "warn", line: 1, title: "Manifest is long (consumes context)", desc: `${lineCount} lines. Consider splitting subsystems into nested \`AGENTS.md\` files.` });
155
+ const todoRegex = /(TODO|FIXME|XXX)[:\s]/g;
156
+ for (let i = 0; i < lines.length; i++) {
157
+ if (todoRegex.test(lines[i])) findings.push({ id: `F${id++}`, rule: "E7", pillar: "E", severity: "info", line: i + 1, title: "Unresolved TODO marker", desc: "An agent will treat this as definitive. Resolve before shipping the manifest." });
158
+ todoRegex.lastIndex = 0;
159
+ }
160
+ for (const h of headers) {
161
+ if (h.level >= 5) findings.push({ id: `F${id++}`, rule: "D4", pillar: "D", severity: "info", line: h.line, title: "Heading nested 5+ levels deep", desc: `\`${"#".repeat(h.level)} ${h.title}\` \u2014 flatten or move into a sub-document.` });
162
+ }
163
+ if (!headers.some((h) => h.level === 1)) {
164
+ findings.push({ id: `F${id++}`, rule: "D1", pillar: "D", severity: "warn", line: 1, title: "Manifest has no top-level title (#)", desc: "Start with `# {Project name} \u2014 agent manifest` so agents anchor on identity." });
165
+ }
166
+ const sevOrder = { crit: 0, warn: 1, info: 2 };
167
+ findings.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || a.line - b.line);
168
+ return findings;
169
+ }
170
+ function analyze(content) {
171
+ const headers = parseHeaders(content);
172
+ const sections = buildSections(content, headers);
173
+ const checklist = {};
174
+ for (const spec of EXPECTED_SECTIONS) {
175
+ checklist[spec.key] = gradeSection(spec, matchSection(sections, spec));
176
+ }
177
+ const findings = buildFindings(content, headers, sections, checklist);
178
+ const pillars = computePillarScores(checklist, content, headers);
179
+ const overall = Math.round(PILLARS.reduce((acc, p) => acc + pillars[p.key], 0) / PILLARS.length);
180
+ const outline = headers.map((h) => ({ line: h.line, level: h.level, title: h.title }));
181
+ const lineCount = content.split("\n").length;
182
+ const wordCount = content.replace(/```[\s\S]*?```/g, "").split(/\s+/).filter(Boolean).length;
183
+ return { outline, findings, pillars, score: overall, checklist, stats: { lines: lineCount, words: wordCount, headers: headers.length } };
184
+ }
185
+
186
+ // ../../packages/mcp-core/src/ci-workflow.ts
187
+ var DEFAULT_CLI_VERSION = "0.1.11";
188
+ function buildCiWorkflowYaml(options) {
189
+ const {
190
+ minScore,
191
+ minPillarScore = null,
192
+ uploadSarif = false,
193
+ uploadArtifact = false,
194
+ cliVersion = DEFAULT_CLI_VERSION,
195
+ ignorePatterns = [],
196
+ useCompositeAction = false,
197
+ actionRef = "v1"
198
+ } = options;
199
+ const ignoreFlag = ignorePatterns.length > 0 ? ` --ignore '${ignorePatterns.join(",")}'` : "";
200
+ const permissionsBlock = uploadSarif ? `
201
+ permissions:
202
+ contents: read
203
+ security-events: write
204
+ ` : "";
205
+ const scoreStep = useCompositeAction ? buildCompositeActionStep({ minScore, minPillarScore, uploadSarif, uploadArtifact, cliVersion, ignorePatterns, actionRef }) : buildNpxScoreSteps({ minScore, minPillarScore, uploadSarif, uploadArtifact, cliVersion, ignoreFlag });
206
+ return `# Generated by Clarx Cloud \u2014 clarx.ai/docs/ci
207
+ name: Clarx
208
+ on:
209
+ pull_request:
210
+ types: [opened, synchronize, reopened]
211
+ ${permissionsBlock}
212
+ jobs:
213
+ clarx:
214
+ runs-on: ubuntu-latest
215
+ steps:
216
+ - uses: actions/checkout@v4
217
+ ${scoreStep}
218
+ `;
219
+ }
220
+ function buildCompositeActionStep(opts) {
221
+ const lines = [
222
+ ` - uses: clarxai/clarx-cloud/action@${opts.actionRef}`,
223
+ " with:",
224
+ ` min-score: '${opts.minScore}'`
225
+ ];
226
+ if (opts.minPillarScore !== null) {
227
+ lines.push(` min-pillar-score: '${opts.minPillarScore}'`);
228
+ }
229
+ if (opts.ignorePatterns.length > 0) {
230
+ lines.push(` ignore: '${opts.ignorePatterns.join(",")}'`);
231
+ }
232
+ lines.push(` cli-version: '${opts.cliVersion}'`);
233
+ if (opts.uploadSarif) lines.push(` upload-sarif: 'true'`);
234
+ if (opts.uploadArtifact) lines.push(` upload-artifact: 'true'`);
235
+ return lines.join("\n");
236
+ }
237
+ function buildNpxScoreSteps(opts) {
238
+ const pillarFlag = opts.minPillarScore !== null ? ` --min-pillar-score ${opts.minPillarScore}` : "";
239
+ const lines = [
240
+ "",
241
+ " - uses: actions/setup-node@v4",
242
+ " with:",
243
+ " node-version: 22",
244
+ "",
245
+ " - name: Score codebase",
246
+ ` run: npx @clarxai/cli@${opts.cliVersion} score . --ui text --min-score ${opts.minScore}${pillarFlag}${opts.ignoreFlag}`
247
+ ];
248
+ if (opts.uploadSarif) {
249
+ lines.push(
250
+ "",
251
+ " - name: Generate SARIF",
252
+ " if: always()",
253
+ ` run: npx @clarxai/cli@${opts.cliVersion} score . --format sarif${opts.ignoreFlag} > clarx.sarif`,
254
+ "",
255
+ " - name: Upload SARIF",
256
+ " if: always()",
257
+ " uses: github/codeql-action/upload-sarif@v3",
258
+ " with:",
259
+ " sarif_file: clarx.sarif",
260
+ " category: clarx"
261
+ );
262
+ }
263
+ if (opts.uploadArtifact) {
264
+ lines.push(
265
+ "",
266
+ " - name: Markdown report",
267
+ " if: always()",
268
+ ` run: npx @clarxai/cli@${opts.cliVersion} score . --format markdown${opts.ignoreFlag} > clarx-report.md`,
269
+ "",
270
+ " - uses: actions/upload-artifact@v4",
271
+ " if: always()",
272
+ " with:",
273
+ " name: clarx-report",
274
+ " path: clarx-report.md"
275
+ );
276
+ }
277
+ return lines.join("\n");
278
+ }
279
+
280
+ // ../../packages/mcp-core/src/machine-manifest.ts
281
+ var KNOWN_KEYS = ["generated", "highFanIn", "highFanOut", "verificationCommands", "commonTasks", "workspaces", "thresholds"];
282
+ var KEY_HINTS = {
283
+ generated: "Directories agents should treat as build output, not source.",
284
+ highFanIn: "Files many modules import \u2014 agents edit these with extra care.",
285
+ highFanOut: "Known hub files (routers, registries) exempted from fan-out rules.",
286
+ verificationCommands: "How an agent verifies its changes. Closes O1 and unlocks the B-pillar.",
287
+ commonTasks: "Recipes for the changes agents are asked to make most often.",
288
+ workspaces: "Monorepo workspace roots.",
289
+ thresholds: "Per-repo overrides of engine defaults (e.g. maxFileLines). Rendered in rule messages."
290
+ };
291
+ function analyzeMachineManifest(content) {
292
+ let parsed;
293
+ try {
294
+ parsed = JSON.parse(content);
295
+ } catch (err) {
296
+ return { parseError: err instanceof Error ? err.message : "Invalid JSON", knownKeys: [], unknownKeys: [], thresholdIssues: [] };
297
+ }
298
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
299
+ return { parseError: "Top level must be a JSON object.", knownKeys: [], unknownKeys: [], thresholdIssues: [] };
300
+ }
301
+ const obj = parsed;
302
+ const keys = Object.keys(obj);
303
+ const knownKeys = keys.filter((k) => KNOWN_KEYS.includes(k));
304
+ const unknownKeys = keys.filter((k) => !KNOWN_KEYS.includes(k));
305
+ const thresholdIssues = [];
306
+ if (obj.thresholds !== void 0) {
307
+ if (typeof obj.thresholds !== "object" || obj.thresholds === null || Array.isArray(obj.thresholds)) {
308
+ thresholdIssues.push("thresholds must be an object of numeric overrides.");
309
+ } else {
310
+ for (const [k, v] of Object.entries(obj.thresholds)) {
311
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
312
+ thresholdIssues.push(`thresholds.${k} must be a finite positive number (got ${JSON.stringify(v)}).`);
313
+ }
314
+ }
315
+ }
316
+ }
317
+ return { parseError: null, knownKeys, unknownKeys, thresholdIssues };
318
+ }
319
+
320
+ // ../../packages/mcp-core/src/prompts.ts
321
+ var MANIFEST_AI_MODEL = "claude-haiku-4-5-20251001";
322
+ var MANIFEST_FIX_MAX_TOKENS = 800;
323
+ var MANIFEST_DRAFT_MAX_TOKENS = 2500;
324
+ function getSectionSpec(sectionKey) {
325
+ return EXPECTED_SECTIONS.find((s) => s.key === sectionKey);
326
+ }
327
+ function buildManifestFixPrompt(input) {
328
+ const {
329
+ sectionLabel,
330
+ hint,
331
+ pillar,
332
+ minWords,
333
+ needsCode,
334
+ needsStructure,
335
+ currentContent,
336
+ docPath,
337
+ repoName,
338
+ repoDescription,
339
+ repoReadme
340
+ } = input;
341
+ const system = `You are an expert at writing CLAUDE.md and AGENTS.md manifest files \u2014 structured documents that help AI coding agents understand a codebase.
342
+
343
+ Your task is to write a single well-formed markdown section that fixes a specific quality issue.
344
+
345
+ You must satisfy the validator exactly:
346
+ - Return ONLY the raw markdown for one section, starting with ## or ###.
347
+ - Use the requested section heading verbatim.
348
+ - Include enough prose outside fenced code blocks to satisfy any minimum word count.
349
+ - If a code block is required, include one.
350
+ - If structure is required, include either a bullet list or a code block, and make the structure explicit.
351
+ - Do not return placeholder text, generic advice, or commentary about what you are doing.`;
352
+ const repoContextLines = [];
353
+ if (repoName) repoContextLines.push(`**Repository name:** ${repoName}`);
354
+ if (repoDescription) repoContextLines.push(`**GitHub description:** ${repoDescription}`);
355
+ if (repoReadme) repoContextLines.push(`**README (excerpt):**
356
+ \`\`\`
357
+ ${repoReadme.slice(0, 2500)}
358
+ \`\`\``);
359
+ const repoContext = repoContextLines.length > 0 ? repoContextLines.join("\n") + "\n\n" : "";
360
+ const validatorRules = [
361
+ `Use the heading \`## ${sectionLabel}\` unless a \`###\` subsection is clearly more appropriate.`,
362
+ typeof minWords === "number" ? `Include at least ${minWords} words of prose outside fenced code blocks.` : null,
363
+ needsCode ? "Include at least one fenced code block in the section." : null,
364
+ needsStructure ? "Make the section structurally explicit with a bullet list or a code block." : null,
365
+ "Do not rely on a code block alone; include explanatory prose before and/or after it."
366
+ ].filter(Boolean).join("\n- ");
367
+ const user = `The manifest file at \`${docPath}\` has a quality issue:
368
+
369
+ ${repoContext}**Section needed:** ${sectionLabel} (pillar: ${pillar})
370
+ **Guidance:** ${hint}
371
+ **Validator rules:**
372
+ - ${validatorRules}
373
+
374
+ Here is the current full content of the manifest:
375
+ \`\`\`markdown
376
+ ${currentContent.slice(0, 2e3)}
377
+ \`\`\`
378
+
379
+ Write the missing or improved \`## ${sectionLabel}\` section. Base it on the real repository information above \u2014 do not invent generic placeholder content. Keep it concise, but make sure it passes the validator rules above.`;
380
+ return { model: MANIFEST_AI_MODEL, maxTokens: MANIFEST_FIX_MAX_TOKENS, system, user };
381
+ }
382
+ function buildManifestDraftPrompt(input) {
383
+ const { filename, repoName, repoDescription, repoReadme } = input;
384
+ const agentName = filename.replace(/\.md$/i, "");
385
+ const system = `You are an expert at writing ${filename} manifest files \u2014 structured documents that teach AI coding agents how to work in a specific codebase.
386
+
387
+ Write a complete first draft grounded ONLY in the repository information provided. Rules:
388
+ - Return ONLY raw markdown, starting with \`# ${filename}\`.
389
+ - Include these H2 sections in this order: Overview, Architecture, Build & Setup, Testing, Code conventions, Directory structure, Entry points.
390
+ - Ground every claim in the README/description. Where the README does not say (e.g. exact test command), write a clearly-marked TODO line like \`<!-- TODO: confirm test command -->\` followed by your best inference \u2014 never invent specifics as fact.
391
+ - Overview: 2-4 sentences on what the product is and who uses it.
392
+ - Build & Setup and Directory structure must each include a fenced code block.
393
+ - Keep the whole document under 120 lines. Concise and specific beats long and generic.`;
394
+ const contextLines = [];
395
+ if (repoName) contextLines.push(`**Repository:** ${repoName}`);
396
+ if (repoDescription) contextLines.push(`**Description:** ${repoDescription}`);
397
+ if (repoReadme) contextLines.push(`**README:**
398
+ \`\`\`
399
+ ${repoReadme.slice(0, 6e3)}
400
+ \`\`\``);
401
+ const user = `Draft ${filename} for this repository (guidance file for ${agentName} and similar AI agents):
402
+
403
+ ${contextLines.join("\n\n")}`;
404
+ return { model: MANIFEST_AI_MODEL, maxTokens: MANIFEST_DRAFT_MAX_TOKENS, system, user };
405
+ }
406
+
407
+ // src/anthropic.ts
408
+ async function callAnthropic(prompt, apiKey) {
409
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
410
+ method: "POST",
411
+ headers: {
412
+ "x-api-key": apiKey,
413
+ "anthropic-version": "2023-06-01",
414
+ "content-type": "application/json"
415
+ },
416
+ body: JSON.stringify({
417
+ model: prompt.model,
418
+ max_tokens: prompt.maxTokens,
419
+ system: prompt.system,
420
+ messages: [{ role: "user", content: prompt.user }]
421
+ })
422
+ });
423
+ if (!res.ok) {
424
+ const err = await res.text().catch(() => "unknown error");
425
+ throw new Error(`Anthropic API error (${res.status}): ${err}`);
426
+ }
427
+ const data = await res.json();
428
+ const text = data.content?.find((b) => b.type === "text")?.text ?? "";
429
+ if (!text.trim()) throw new Error("No content returned from the model.");
430
+ return text;
431
+ }
432
+
433
+ // src/local-tools.ts
434
+ function jsonResult(value) {
435
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
436
+ }
437
+ function errorResult(message) {
438
+ return { content: [{ type: "text", text: message }], isError: true };
439
+ }
440
+ async function resolveContent(args) {
441
+ if (args.content !== void 0 && args.file_path !== void 0) {
442
+ return { error: "Pass exactly one of `content` or `file_path`, not both." };
443
+ }
444
+ if (args.content !== void 0) return { content: args.content, path: null };
445
+ if (args.file_path !== void 0) {
446
+ const abs = resolve(process.cwd(), args.file_path);
447
+ try {
448
+ return { content: await readFile(abs, "utf8"), path: args.file_path };
449
+ } catch (err) {
450
+ return { error: `Could not read ${abs}: ${err instanceof Error ? err.message : String(err)}` };
451
+ }
452
+ }
453
+ return { error: "Pass one of `content` or `file_path`." };
454
+ }
455
+ function registerLocalTools(server2) {
456
+ server2.registerTool(
457
+ "analyze_manifest",
458
+ {
459
+ title: "Score an agent manifest",
460
+ description: "Score CLAUDE.md / AGENTS.md manifest content with the same rules as the Clarx manifest studio. Returns an overall quality estimate (0-100), per-pillar scores, findings with line numbers, and a section checklist. This is a manifest quality estimate \u2014 do not compare it to Clarx repo AI-readiness scores, which use a different rule system.",
461
+ inputSchema: {
462
+ content: z.string().optional().describe("Full markdown body of the manifest. Pass this or file_path, not both."),
463
+ file_path: z.string().optional().describe("Path to the manifest file (resolved against the server working directory, typically the workspace root). Preferred \u2014 avoids inlining the file into the conversation."),
464
+ include_outline: z.boolean().optional().default(true).describe("Include the heading outline (section tree with line numbers).")
465
+ }
466
+ },
467
+ async (args) => {
468
+ const resolved = await resolveContent(args);
469
+ if ("error" in resolved) return errorResult(resolved.error);
470
+ const result = analyze(resolved.content);
471
+ return jsonResult({
472
+ path: resolved.path,
473
+ line_count: result.stats.lines,
474
+ overall_score: result.score,
475
+ score_type: "manifest_quality_estimate",
476
+ pillar_scores: PILLARS.map((p) => ({ pillar: p.key, name: p.name, score: result.pillars[p.key] })),
477
+ findings: result.findings.map((f) => ({
478
+ id: f.id,
479
+ rule: f.rule,
480
+ pillar: f.pillar,
481
+ severity: f.severity,
482
+ line: f.line,
483
+ title: f.title,
484
+ description: f.desc,
485
+ section_key: f.sectionKey
486
+ })),
487
+ sections: EXPECTED_SECTIONS.map((spec) => {
488
+ const entry = result.checklist[spec.key];
489
+ return { key: spec.key, label: spec.label, present: entry.present, score: Math.round(entry.score * 100) };
490
+ }),
491
+ ...args.include_outline !== false ? { outline: result.outline } : {}
492
+ });
493
+ }
494
+ );
495
+ server2.registerTool(
496
+ "analyze_clarx_manifest",
497
+ {
498
+ title: "Validate clarx-manifest.json",
499
+ description: "Validate a clarx-manifest.json machine manifest against the Clarx engine schema. Returns recognized keys, errors, and suggestions for keys worth declaring (generated dirs, hubs, verification commands).",
500
+ inputSchema: {
501
+ content: z.string().optional().describe("Raw JSON content. Pass this or file_path, not both."),
502
+ file_path: z.string().optional().describe("Path to clarx-manifest.json (resolved against the server working directory).")
503
+ }
504
+ },
505
+ async (args) => {
506
+ const resolved = await resolveContent(args);
507
+ if ("error" in resolved) return errorResult(resolved.error);
508
+ const a = analyzeMachineManifest(resolved.content);
509
+ const errors = a.parseError ? [a.parseError] : [...a.thresholdIssues];
510
+ const suggestions = [];
511
+ if (!a.parseError) {
512
+ for (const k of a.unknownKeys) {
513
+ suggestions.push(`Remove or rename unknown key "${k}" \u2014 the engine ignores it. Valid keys: ${KNOWN_KEYS.join(", ")}.`);
514
+ }
515
+ for (const k of ["generated", "verificationCommands", "highFanIn", "highFanOut"]) {
516
+ if (!a.knownKeys.includes(k)) suggestions.push(`Consider adding "${k}": ${KEY_HINTS[k]}`);
517
+ }
518
+ }
519
+ return jsonResult({
520
+ valid: errors.length === 0,
521
+ errors,
522
+ keys_recognized: a.knownKeys,
523
+ suggestions
524
+ });
525
+ }
526
+ );
527
+ server2.registerTool(
528
+ "get_ci_workflow",
529
+ {
530
+ title: "Generate Clarx CI workflow",
531
+ description: "Generate a GitHub Actions workflow YAML that gates pull requests on the Clarx AI-readiness score (npx @clarxai/cli). Write the result to .github/workflows/clarx.yml.",
532
+ inputSchema: {
533
+ min_score: z.number().optional().default(80).describe("Fail the check when the overall score is below this (0-100)."),
534
+ min_pillar_score: z.number().nullable().optional().default(null).describe("Optional per-pillar floor."),
535
+ upload_sarif: z.boolean().optional().default(false).describe("Upload findings as SARIF to GitHub code scanning."),
536
+ upload_artifact: z.boolean().optional().default(true).describe("Attach a markdown report artifact to the run."),
537
+ ignore_patterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude from scoring, e.g. ['vendor/**']."),
538
+ cli_version: z.string().optional().default(DEFAULT_CLI_VERSION).describe("@clarxai/cli version to pin.")
539
+ }
540
+ },
541
+ async (args) => {
542
+ const yaml = buildCiWorkflowYaml({
543
+ minScore: args.min_score ?? 80,
544
+ minPillarScore: args.min_pillar_score ?? null,
545
+ uploadSarif: args.upload_sarif ?? false,
546
+ uploadArtifact: args.upload_artifact ?? true,
547
+ ignorePatterns: args.ignore_patterns ?? [],
548
+ cliVersion: args.cli_version ?? DEFAULT_CLI_VERSION
549
+ });
550
+ return jsonResult({ filename: ".github/workflows/clarx.yml", yaml });
551
+ }
552
+ );
553
+ const anthropicKey = process.env.ANTHROPIC_API_KEY?.trim();
554
+ if (!anthropicKey) {
555
+ console.error("[clarx-mcp] ANTHROPIC_API_KEY not set \u2014 AI tools (suggest_manifest_fix, generate_manifest_draft) disabled.");
556
+ return;
557
+ }
558
+ server2.registerTool(
559
+ "suggest_manifest_fix",
560
+ {
561
+ title: "AI-write a manifest section",
562
+ description: "Write one well-formed markdown section of an agent manifest (CLAUDE.md / AGENTS.md) to close a finding from analyze_manifest. Uses your ANTHROPIC_API_KEY. Returns the section markdown to insert.",
563
+ inputSchema: {
564
+ section_key: z.string().describe(`Section to write. One of: ${EXPECTED_SECTIONS.map((s) => s.key).join(", ")}.`),
565
+ current_content: z.string().optional().describe("Current full manifest content for context (may be empty for a new file). Pass this or file_path."),
566
+ file_path: z.string().optional().describe("Read the current manifest from this path instead of inlining it."),
567
+ doc_path: z.string().optional().default("CLAUDE.md").describe("Manifest path used in the prompt, e.g. CLAUDE.md."),
568
+ repo_name: z.string().optional(),
569
+ repo_description: z.string().nullable().optional(),
570
+ repo_readme: z.string().nullable().optional().describe("README excerpt for grounding (max ~2500 chars used).")
571
+ }
572
+ },
573
+ async (args) => {
574
+ const spec = getSectionSpec(args.section_key);
575
+ if (!spec) {
576
+ return errorResult(`Unknown section_key "${args.section_key}". Valid keys: ${EXPECTED_SECTIONS.map((s) => s.key).join(", ")}.`);
577
+ }
578
+ let current = args.current_content;
579
+ let docPath = args.doc_path ?? "CLAUDE.md";
580
+ if (current === void 0 && args.file_path !== void 0) {
581
+ const resolved = await resolveContent({ file_path: args.file_path });
582
+ if ("error" in resolved) return errorResult(resolved.error);
583
+ current = resolved.content;
584
+ docPath = args.file_path;
585
+ }
586
+ if (current === void 0) return errorResult("Pass one of `current_content` or `file_path` (current_content may be an empty string for a new manifest).");
587
+ const prompt = buildManifestFixPrompt({
588
+ sectionLabel: spec.label,
589
+ hint: spec.hint,
590
+ pillar: spec.pillar,
591
+ minWords: spec.minWords,
592
+ needsCode: spec.needsCode,
593
+ needsStructure: spec.needsStructure,
594
+ currentContent: current,
595
+ docPath,
596
+ repoName: args.repo_name,
597
+ repoDescription: args.repo_description,
598
+ repoReadme: args.repo_readme
599
+ });
600
+ try {
601
+ const markdown = await callAnthropic(prompt, anthropicKey);
602
+ return jsonResult({ section_markdown: markdown, section_key: spec.key, pillar: spec.pillar });
603
+ } catch (err) {
604
+ return errorResult(err instanceof Error ? err.message : String(err));
605
+ }
606
+ }
607
+ );
608
+ server2.registerTool(
609
+ "generate_manifest_draft",
610
+ {
611
+ title: "AI-draft a full manifest",
612
+ description: "Generate a first-draft agent manifest (CLAUDE.md / AGENTS.md / GEMINI.md) grounded in the repository README and description. Uses your ANTHROPIC_API_KEY. Claims the model cannot ground are marked as TODO comments.",
613
+ inputSchema: {
614
+ filename: z.enum(["CLAUDE.md", "AGENTS.md", "GEMINI.md"]).describe("Manifest filename to draft."),
615
+ repo_name: z.string().optional(),
616
+ repo_description: z.string().nullable().optional(),
617
+ repo_readme: z.string().optional().describe("README content for grounding (max ~6000 chars used). Required unless repo_description is present.")
618
+ }
619
+ },
620
+ async (args) => {
621
+ if (!args.repo_readme && !args.repo_description) {
622
+ return errorResult("No repository context \u2014 pass repo_readme (preferred) or repo_description to draft from.");
623
+ }
624
+ const prompt = buildManifestDraftPrompt({
625
+ filename: args.filename,
626
+ repoName: args.repo_name,
627
+ repoDescription: args.repo_description,
628
+ repoReadme: args.repo_readme
629
+ });
630
+ try {
631
+ const markdown = await callAnthropic(prompt, anthropicKey);
632
+ return jsonResult({ filename: args.filename, markdown });
633
+ } catch (err) {
634
+ return errorResult(err instanceof Error ? err.message : String(err));
635
+ }
636
+ }
637
+ );
638
+ }
639
+
640
+ // src/index.ts
641
+ var server = new McpServer({ name: "clarx", version: "0.1.0" });
642
+ registerLocalTools(server);
643
+ if (process.env.CLARX_MCP_TOKEN) {
644
+ console.error("[clarx-mcp] CLARX_MCP_TOKEN detected, but hosted tools are not available in this version. Local tools only.");
645
+ }
646
+ var transport = new StdioServerTransport();
647
+ await server.connect(transport);
648
+ console.error("[clarx-mcp] ready (stdio)");
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@clarxai/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Clarx MCP server — score agent manifests, validate clarx-manifest.json, and generate CI workflows from your IDE. Set CLARX_MCP_TOKEN to unlock hosted scan data (coming soon).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "clarx-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "modelcontextprotocol",
19
+ "clarx",
20
+ "ai-readiness",
21
+ "claude-md",
22
+ "agents-md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "lint": "tsc --noEmit",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.0",
31
+ "zod": "^3.25.0"
32
+ },
33
+ "devDependencies": {
34
+ "@clarxai/mcp-core": "workspace:^",
35
+ "@types/node": "^24.0.0",
36
+ "tsup": "^8.5.0",
37
+ "typescript": "^5.8.3"
38
+ }
39
+ }