contentrain_doctor
Checks project health for Contentrain projects: git, node, project structure, model parsing, orphan content, branch pressure, and SDK freshness. Optionally performs deeper analysis to find unused keys, duplicate values, and missing locales.
Instructions
Project health report (read-only). Returns structured checks: git, node, .contentrain/ structure, model parse, orphan content, branch pressure, SDK freshness. Pass usage: true for a deeper analysis of content-key references in source files (unused keys, duplicate dictionary values, locale coverage). Local-filesystem only — unavailable over remote providers.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| usage | No | Run the heavier usage-analysis branch (unused keys, duplicate values, missing locales). Default: false. |
Implementation Reference
- packages/mcp/src/core/doctor.ts:92-276 (handler)runDoctor() — core function that executes the doctor health-check logic. Runs 9+ checks (Git, Node, .contentrain/ structure, config, models, orphan content, branches, SDK freshness, and optionally usage analysis for unused keys, duplicate values, and locale coverage). Returns a structured DoctorReport JSON.
export async function runDoctor( projectRoot: string, options: RunDoctorOptions = {}, ): Promise<DoctorReport> { const checks: DoctorCheck[] = [] // ─── 1. Git installed ─── try { const git = simpleGit(projectRoot) const version = await git.version() checks.push({ name: 'Git', pass: true, detail: `v${version.major}.${version.minor}.${version.patch}`, }) } catch { checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' }) } // ─── 2. Git repo initialized ─── const hasGit = await pathExists(join(projectRoot, '.git')) checks.push({ name: 'Git repository', pass: hasGit, detail: hasGit ? projectRoot : 'No .git directory found', severity: hasGit ? undefined : 'error', }) // ─── 3. Node version ─── const nodeVersion = process.versions.node const [major] = nodeVersion.split('.').map(Number) const nodePass = (major ?? 0) >= 22 checks.push({ name: 'Node.js', pass: nodePass, detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`, severity: nodePass ? undefined : 'error', }) // ─── 4. .contentrain/ structure ─── const crDir = contentrainDir(projectRoot) const hasCrDir = await pathExists(crDir) const hasConfig = await pathExists(join(crDir, 'config.json')) const hasModels = await pathExists(join(crDir, 'models')) const hasContent = await pathExists(join(crDir, 'content')) const structurePass = hasCrDir && hasConfig && hasModels && hasContent checks.push({ name: '.contentrain/ structure', pass: structurePass, detail: !hasCrDir ? 'Not initialized — run `contentrain init`' : [ hasConfig ? null : 'missing config.json', hasModels ? null : 'missing models/', hasContent ? null : 'missing content/', ].filter(Boolean).join(', ') || 'OK', severity: structurePass ? undefined : 'error', }) // ─── 5. Config parseable ─── let config: ContentrainConfig | null = null if (hasConfig) { config = await readConfig(projectRoot) checks.push({ name: 'Config', pass: config !== null, detail: config ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}` : 'Failed to parse config.json', severity: config ? undefined : 'error', }) } // ─── 6. Models all parseable ─── if (hasCrDir) { try { const models = await listModels(projectRoot) const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id))) const allParseable = parseResults.every(r => r !== null) checks.push({ name: 'Models', pass: allParseable, detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`, severity: allParseable ? undefined : 'error', }) } catch { checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' }) } } // ─── 7. Orphan content ─── if (hasCrDir) { const orphans = await findOrphanContent(projectRoot) checks.push({ name: 'Orphan content', pass: orphans.length === 0, detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`, severity: orphans.length === 0 ? undefined : 'warning', }) } // ─── 8. Stale contentrain branches ─── if (hasGit) { try { const health = await checkBranchHealth(projectRoot) checks.push({ name: 'Pending branches', pass: !health.blocked && !health.warning, detail: health.message ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`), severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined, }) } catch { checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' }) } } // ─── 9. SDK client freshness ─── const clientDir = join(crDir, 'client') const modelsDir = join(crDir, 'models') if (await pathExists(clientDir) && await pathExists(modelsDir)) { try { const [clientStat, modelsStat] = await Promise.all([stat(clientDir), stat(modelsDir)]) const fresh = clientStat.mtimeMs >= modelsStat.mtimeMs checks.push({ name: 'SDK client', pass: fresh, detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`', severity: fresh ? undefined : 'warning', }) } catch { checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' }) } } // ─── 10–12. Usage analysis (optional) ─── let usage: DoctorUsageAnalysis | undefined if (options.usage && hasCrDir && config) { const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([ analyzeUnusedKeys(projectRoot, config), analyzeDuplicateValues(projectRoot, config), analyzeMissingLocaleKeys(projectRoot, config), ]) usage = { unusedKeys, duplicateValues, missingLocaleKeys } checks.push({ name: 'Unused content keys', pass: unusedKeys.length === 0, detail: unusedKeys.length === 0 ? 'All keys referenced in source' : `${unusedKeys.length} key(s) not referenced in source code`, severity: unusedKeys.length === 0 ? undefined : 'warning', }) checks.push({ name: 'Duplicate dictionary values', pass: duplicateValues.length === 0, detail: duplicateValues.length === 0 ? 'No duplicate values' : `${duplicateValues.length} value(s) mapped to multiple keys`, severity: duplicateValues.length === 0 ? undefined : 'warning', }) checks.push({ name: 'Locale key coverage', pass: missingLocaleKeys.length === 0, detail: missingLocaleKeys.length === 0 ? 'All locales have matching keys' : `${missingLocaleKeys.length} key(s) missing in some locales`, severity: missingLocaleKeys.length === 0 ? undefined : 'warning', }) } const passed = checks.filter(c => c.pass).length const failed = checks.length - passed const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length const report: DoctorReport = { checks, summary: { total: checks.length, passed, failed, warnings }, } if (usage) report.usage = usage return report } - packages/mcp/src/tools/doctor.ts:8-29 (registration)registerDoctorTools() — registers the 'contentrain_doctor' MCP tool on the server via server.tool(). Defines the 'usage' boolean parameter, attaches annotations, and calls runDoctor() from the core.
export function registerDoctorTools( server: McpServer, _provider: ToolProvider, projectRoot: string | undefined, ): void { server.tool( 'contentrain_doctor', 'Project health report (read-only). Returns structured checks: git, node, .contentrain/ structure, model parse, orphan content, branch pressure, SDK freshness. Pass `usage: true` for a deeper analysis of content-key references in source files (unused keys, duplicate dictionary values, locale coverage). Local-filesystem only — unavailable over remote providers.', { usage: z.boolean().optional().default(false).describe('Run the heavier usage-analysis branch (unused keys, duplicate values, missing locales). Default: false.'), }, TOOL_ANNOTATIONS['contentrain_doctor']!, async ({ usage }) => { if (!projectRoot) return capabilityError('contentrain_doctor', 'localWorktree') const report = await runDoctor(projectRoot, { usage }) return { content: [{ type: 'text' as const, text: JSON.stringify(report, null, 2) }], } }, ) } - Tool annotation for 'contentrain_doctor': readOnlyHint=true, destructiveHint=false, idempotentHint=true, title='Project Health Report'.
contentrain_doctor: { title: 'Project Health Report', readOnlyHint: true, destructiveHint: false, idempotentHint: true, }, - packages/rules/src/index.ts:37-37 (schema)'contentrain_doctor' listed in MCP_TOOLS constant (17th tool in the canonical tool name list).
'contentrain_doctor', - packages/mcp/src/server.ts:20-20 (registration)Import of registerDoctorTools in the central server.ts, called at line 74 to register the doctor tool.
import { registerDoctorTools } from './tools/doctor.js'