🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@codehabits/mcp

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@codehabits/mcp - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2
+874
dist/chunk-KUSOV7Y3.js
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z as z4 } from "zod";
// src/intelligence/reader.ts
import { readFileSync, existsSync, watchFile, unwatchFile } from "fs";
import { join } from "path";
// ../shared/src/emit-targets.ts
import { z } from "zod";
var emitTargetSchema = z.enum([
"agents-md",
"claude-skills",
"agents-skills",
"cursor-skills"
]);
// ../shared/src/schemas/intelligence.ts
import { z as z2 } from "zod";
var intelligenceSourceSchema = z2.enum([
"code_analysis",
"config_import",
"stack_baseline",
"git_history",
"pr_analysis",
"rejection_analysis",
"comment_analysis",
"agent_proposal"
]);
var maturityLevelSchema = z2.union([
z2.literal(1),
z2.literal(2),
z2.literal(3),
z2.literal(4)
]);
var maturityLabelSchema = z2.enum([
"baseline",
"growing",
"established",
"mature"
]);
var conventionCategorySchema = z2.enum([
"imports",
"testing",
"naming",
"async",
"structure",
"error-handling",
"api",
"documentation",
"security",
"performance",
"type-safety",
"other"
]);
var antiPatternSeveritySchema = z2.enum([
"critical",
"high",
"medium",
"low"
]);
var techStackSchema = z2.object({
language: z2.string(),
languages: z2.array(z2.string()).optional(),
framework: z2.string().optional(),
framework_version: z2.string().optional(),
test_framework: z2.string().optional(),
linter: z2.string().optional(),
formatter: z2.string().optional(),
build_tool: z2.string().optional(),
package_manager: z2.string().optional()
});
var conventionExampleSchema = z2.object({
pr_number: z2.number().optional(),
file_path: z2.string(),
snippet: z2.string().optional()
});
var conventionSchema = z2.object({
id: z2.string(),
category: conventionCategorySchema,
rule: z2.string(),
confidence: z2.number().min(0).max(1),
source: intelligenceSourceSchema,
evidence_count: z2.number(),
first_seen: z2.string(),
examples: z2.array(conventionExampleSchema),
metadata: z2.record(z2.string(), z2.unknown()).optional()
});
var antiPatternEvidenceTypeSchema = z2.enum([
"rejected_pr",
"review_comment",
"bug_fix"
]);
function normalizeAntiPatternEvidenceType(input) {
if (input === null || input === void 0 || input === "") {
return "review_comment";
}
const s = String(input).trim().toLowerCase().replace(/-/g, "_").replace(/\s+/g, "_");
if (s === "rejected_pr" || s === "rejection" || s === "pr_rejection" || s === "rejected" || s.includes("reject")) {
return "rejected_pr";
}
if (s === "bug_fix" || s === "bugfix" || s === "fix" || s.includes("bug") && s.includes("fix")) {
return "bug_fix";
}
if (s === "review_comment" || s === "comment" || s === "pr_comment" || s === "inline_comment" || s === "discussion" || s === "review" || s.includes("comment")) {
return "review_comment";
}
if (s === "merged_pr" || s === "merged" || s === "pr") {
return "review_comment";
}
return "review_comment";
}
var antiPatternEvidenceSchema = z2.object({
pr_number: z2.coerce.number(),
type: z2.preprocess(
(raw) => normalizeAntiPatternEvidenceType(raw),
antiPatternEvidenceTypeSchema
),
excerpt: z2.string().optional()
});
var antiPatternSchema = z2.object({
id: z2.string(),
pattern: z2.string(),
severity: antiPatternSeveritySchema,
reason: z2.string(),
correct_approach: z2.string(),
evidence: z2.array(antiPatternEvidenceSchema)
});
var knowledgeEntrySchema = z2.object({
value: z2.string(),
confidence: z2.number().min(0).max(1),
source_prs: z2.array(z2.number())
});
var knowledgeGraphSchema = z2.record(
z2.string(),
z2.record(z2.string(), knowledgeEntrySchema)
);
var reviewerExpertiseSchema = z2.object({
area: z2.string(),
confidence: z2.number().min(0).max(1),
review_count: z2.number()
});
var reviewerStatsSchema = z2.object({
total_reviews: z2.number(),
avg_review_time_hours: z2.number(),
approval_rate: z2.number().min(0).max(1)
});
var reviewerProfileSchema = z2.object({
github_login: z2.string(),
expertise: z2.array(reviewerExpertiseSchema),
stats: reviewerStatsSchema
});
var riskFactorSchema = z2.object({
type: z2.enum(["file", "size", "timing", "complexity"]),
description: z2.string(),
value: z2.string(),
risk_multiplier: z2.number(),
evidence: z2.object({
total_prs: z2.number(),
incidents: z2.number()
})
});
var maturitySchema = z2.object({
level: maturityLevelSchema,
label: maturityLabelSchema,
prs_analyzed: z2.number(),
next_level_at: z2.number(),
sources: z2.array(intelligenceSourceSchema)
});
var repositoryInfoSchema = z2.object({
owner: z2.string(),
name: z2.string(),
full_name: z2.string(),
analyzed_prs: z2.number(),
analyzed_period: z2.object({
from: z2.string(),
to: z2.string()
}),
tech_stack: techStackSchema,
codehabits_version: z2.string()
});
var intelligenceFileSchema = z2.object({
version: z2.string(),
generated_at: z2.string(),
last_synced_at: z2.string(),
maturity: maturitySchema,
repository: repositoryInfoSchema,
conventions: z2.array(conventionSchema),
anti_patterns: z2.array(antiPatternSchema),
knowledge_graph: knowledgeGraphSchema,
reviewers: z2.array(reviewerProfileSchema),
risk_factors: z2.array(riskFactorSchema)
});
var conventionsFileSchema = z2.array(conventionSchema);
var antiPatternsFileSchema = z2.array(antiPatternSchema);
var knowledgeFileSchema = knowledgeGraphSchema;
var reviewersFileSchema = z2.array(reviewerProfileSchema);
var intelligenceMetaSchema = z2.object({
version: z2.string(),
generated_at: z2.string(),
last_synced_at: z2.string(),
maturity: maturitySchema,
repository: repositoryInfoSchema,
risk_factors: z2.array(riskFactorSchema),
counts: z2.object({
conventions: z2.number(),
anti_patterns: z2.number(),
reviewers: z2.number(),
knowledge_topics: z2.number()
})
});
var proposalSchema = z2.object({
id: z2.string(),
type: z2.enum(["new_convention", "update_convention", "new_exception", "deprecate"]),
targetId: z2.string().optional(),
data: conventionSchema.partial(),
reason: z2.string(),
filePath: z2.string().optional(),
createdAt: z2.string(),
status: z2.enum(["pending", "approved", "rejected"]),
approvedBy: z2.string().optional()
});
var proposalsFileSchema = z2.array(proposalSchema);
// ../shared/src/schemas/config.ts
import { z as z3 } from "zod";
var customRuleSchema = z3.object({
name: z3.string(),
severity: z3.enum(["error", "warning", "info"]),
description: z3.string()
});
var CodehabitsConfigSchema = z3.object({
version: z3.string(),
analysis: z3.object({
lookback_months: z3.number().default(12),
min_prs_for_pattern: z3.number().default(10),
min_confidence: z3.number().min(0).max(1).default(0.7),
include_closed_prs: z3.boolean().default(true)
}),
ignore: z3.object({
files: z3.array(z3.string()).default([]),
authors: z3.array(z3.string()).default(["dependabot", "renovate"]),
labels: z3.array(z3.string()).default(["wip", "draft"])
}),
sync: z3.object({
auto_commit: z3.boolean().default(false)
}),
/** Where to write derived views (AGENTS.md, per-agent skill dirs). Omitted = use CLI defaults. */
emit_targets: z3.array(emitTargetSchema).optional(),
custom_rules: z3.array(customRuleSchema).default([])
});
var userConfigSchema = z3.object({
auth: z3.object({
token: z3.string(),
github_token: z3.string(),
user: z3.object({
id: z3.string(),
login: z3.string(),
email: z3.string()
})
}).optional(),
defaults: z3.object({
ai_model: z3.string().default("deepseek")
}).optional()
});
// ../shared/src/constants.ts
var SKILL_ROOT_NAME = "codehabits-team-intel";
var PATHS = {
/** @deprecated Use individual file paths instead */
INTELLIGENCE_FILE: ".codehabits/intelligence.json",
CONVENTIONS_FILE: ".codehabits/conventions.json",
ANTI_PATTERNS_FILE: ".codehabits/anti-patterns.json",
KNOWLEDGE_FILE: ".codehabits/knowledge.json",
REVIEWERS_FILE: ".codehabits/reviewers.json",
META_FILE: ".codehabits/meta.json",
PROPOSALS_FILE: ".codehabits/proposals.json",
README_FILE: ".codehabits/README.md",
CODEHABITS_GITIGNORE: ".codehabits/.gitignore",
CONFIG_FILE: ".codehabits/config.json",
/** Claude Code project skills (Cursor also discovers `.claude/skills/`). */
CLAUDE_SKILL_DIR: `.claude/skills/${SKILL_ROOT_NAME}`,
CLAUDE_SKILL_FILE: `.claude/skills/${SKILL_ROOT_NAME}/SKILL.md`,
CLAUDE_SKILL_REFERENCES: `.claude/skills/${SKILL_ROOT_NAME}/references`,
CLAUDE_SKILL_SCRIPTS: `.claude/skills/${SKILL_ROOT_NAME}/scripts`,
/** Cross-client convention (agentskills.io / VS Code Copilot / many installers). */
AGENTS_SKILL_DIR: `.agents/skills/${SKILL_ROOT_NAME}`,
AGENTS_SKILL_FILE: `.agents/skills/${SKILL_ROOT_NAME}/SKILL.md`,
AGENTS_SKILL_REFERENCES: `.agents/skills/${SKILL_ROOT_NAME}/references`,
AGENTS_SKILL_SCRIPTS: `.agents/skills/${SKILL_ROOT_NAME}/scripts`,
/** Legacy Cursor-only layout (optional emit / migration detection). */
LEGACY_CURSOR_SKILL_DIR: `.cursor/skills/${SKILL_ROOT_NAME}`,
LEGACY_CURSOR_SKILL_FILE: `.cursor/skills/${SKILL_ROOT_NAME}/SKILL.md`,
/**
* Primary skill entry path (alias for Claude layout).
* @deprecated Prefer CLAUDE_SKILL_FILE or resolve paths from emit_targets.
*/
SKILL_FILE: `.claude/skills/${SKILL_ROOT_NAME}/SKILL.md`,
/** @deprecated Prefer CLAUDE_SKILL_DIR */
SKILL_DIR: `.claude/skills/${SKILL_ROOT_NAME}`,
/** @deprecated Prefer CLAUDE_SKILL_REFERENCES */
SKILL_REFERENCES: `.claude/skills/${SKILL_ROOT_NAME}/references`,
/** @deprecated Prefer CLAUDE_SKILL_SCRIPTS */
SKILL_SCRIPTS: `.claude/skills/${SKILL_ROOT_NAME}/scripts`,
USER_CONFIG: ".codehabits/config.json"
};
var AI_MODELS = {
FREE: "deepseek/deepseek-v3.2",
PAID: "anthropic/claude-sonnet-4-20250514"
};
var PLAN_LIMITS = {
free: {
private_repos: 1,
pr_lookback: 100,
ai_model: AI_MODELS.FREE
},
team: {
private_repos: Infinity,
pr_lookback: Infinity,
ai_model: AI_MODELS.PAID
},
enterprise: {
private_repos: Infinity,
pr_lookback: Infinity,
ai_model: AI_MODELS.PAID
}
};
// src/intelligence/reader.ts
var SPLIT_FILES = [
PATHS.META_FILE,
PATHS.CONVENTIONS_FILE,
PATHS.ANTI_PATTERNS_FILE,
PATHS.KNOWLEDGE_FILE,
PATHS.REVIEWERS_FILE
];
function readIntelligenceFile(cwd) {
const metaPath = join(cwd, PATHS.META_FILE);
if (existsSync(metaPath)) {
return readSplitFiles(cwd);
}
return readLegacyFile(cwd);
}
function watchIntelligenceFile(cwd, onChange) {
const reload = () => {
onChange(readIntelligenceFile(cwd));
};
const allPaths = [
join(cwd, PATHS.INTELLIGENCE_FILE),
...SPLIT_FILES.map((rel) => join(cwd, rel))
];
for (const abs of allPaths) {
watchFile(abs, { interval: 2e3 }, reload);
}
return () => {
for (const abs of allPaths) {
unwatchFile(abs, reload);
}
};
}
function readSplitFiles(cwd) {
try {
const meta = readJson(join(cwd, PATHS.META_FILE), intelligenceMetaSchema);
if (!meta) return null;
const conventions = readJson(join(cwd, PATHS.CONVENTIONS_FILE), conventionsFileSchema) ?? [];
const antiPatterns = readJson(join(cwd, PATHS.ANTI_PATTERNS_FILE), antiPatternsFileSchema) ?? [];
const knowledgeGraph = readJson(join(cwd, PATHS.KNOWLEDGE_FILE), knowledgeFileSchema) ?? {};
const reviewers = readJson(join(cwd, PATHS.REVIEWERS_FILE), reviewersFileSchema) ?? [];
return {
version: meta.version,
generated_at: meta.generated_at,
last_synced_at: meta.last_synced_at,
maturity: meta.maturity,
repository: meta.repository,
conventions,
anti_patterns: antiPatterns,
knowledge_graph: knowledgeGraph,
reviewers,
risk_factors: meta.risk_factors
};
} catch (error) {
console.error("Failed to read split intelligence files:", error);
return null;
}
}
function readLegacyFile(cwd) {
return readJson(
join(cwd, PATHS.INTELLIGENCE_FILE),
intelligenceFileSchema
);
}
function readJson(filePath, schema) {
if (!existsSync(filePath)) return null;
try {
const raw = readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
return schema.parse(parsed);
} catch (error) {
console.error(`Failed to read ${filePath}:`, error);
return null;
}
}
// src/tools/get-team-context.ts
function handleGetTeamContext(intelligence2, input) {
const scope = input.scope || "all";
const sections = [];
sections.push(`# Team Intelligence: ${intelligence2.repository.full_name}`);
sections.push(
`Maturity: ${intelligence2.maturity.label} | PRs analyzed: ${intelligence2.maturity.prs_analyzed}`
);
sections.push(
`Tech stack: ${intelligence2.repository.tech_stack.language}${intelligence2.repository.tech_stack.framework ? ` / ${intelligence2.repository.tech_stack.framework}` : ""}`
);
sections.push("");
let conventions;
if (scope === "all") {
conventions = intelligence2.conventions.slice(0, 20);
} else {
conventions = intelligence2.conventions.filter((c) => {
if (c.category === scope) return true;
if (input.file_path) {
return isConventionRelevantToFile(c, input.file_path);
}
return false;
});
if (conventions.length < 3) {
conventions = [
...conventions,
...intelligence2.conventions.filter((c) => c.category === scope).slice(0, 5)
];
}
}
if (conventions.length > 0) {
sections.push("## Conventions");
sections.push("");
for (const conv of conventions) {
sections.push(
`- **${conv.rule}** (${conv.category}, ${Math.round(conv.confidence * 100)}% confidence)`
);
}
sections.push("");
}
let antiPatterns;
if (scope === "all") {
antiPatterns = intelligence2.anti_patterns.slice(0, 10);
} else {
antiPatterns = intelligence2.anti_patterns.filter((ap) => {
const patternLower = ap.pattern.toLowerCase();
const scopeLower = scope.toLowerCase();
return patternLower.includes(scopeLower);
});
if (antiPatterns.length === 0) {
antiPatterns = intelligence2.anti_patterns.slice(0, 5);
}
}
if (antiPatterns.length > 0) {
sections.push("## Anti-Patterns (Avoid These)");
sections.push("");
for (const ap of antiPatterns) {
sections.push(
`- **${ap.pattern}** [${ap.severity}] - ${ap.correct_approach}`
);
}
sections.push("");
}
if (scope !== "all") {
const relevantTopics = Object.entries(intelligence2.knowledge_graph).filter(
([topic]) => topic.toLowerCase().includes(scope.toLowerCase())
);
if (relevantTopics.length > 0) {
sections.push("## Domain Knowledge");
sections.push("");
for (const [topic, entries] of relevantTopics) {
sections.push(`### ${topic}`);
for (const [key, entry] of Object.entries(entries)) {
sections.push(`- ${key}: ${entry.value}`);
}
}
sections.push("");
}
}
if (input.file_path) {
const fileReviewers = intelligence2.reviewers.filter(
(r) => r.expertise.some(
(e) => e.area.split("/").some((part) => input.file_path.includes(part))
)
).slice(0, 3);
if (fileReviewers.length > 0) {
sections.push("## Suggested Reviewers for This File");
sections.push("");
for (const r of fileReviewers) {
sections.push(
`- @${r.github_login} (${r.stats.total_reviews} reviews)`
);
}
}
}
return sections.join("\n");
}
function isConventionRelevantToFile(conv, filePath) {
const fp = filePath.toLowerCase();
if (fp.includes(".test.") || fp.includes(".spec.") || fp.includes("__tests__")) {
return conv.category === "testing";
}
if (fp.includes("/api/") || fp.includes("route.ts") || fp.includes("handler")) {
return conv.category === "api" || conv.category === "error-handling";
}
if (fp.includes("component") || fp.endsWith(".tsx")) {
return conv.category === "naming" || conv.category === "structure";
}
return false;
}
// src/tools/check-code.ts
function handleCheckCode(intelligence2, input) {
const violations = [];
const code = input.code;
const filePath = input.file_path || "";
for (const conv of intelligence2.conventions) {
if (conv.confidence < 0.7) continue;
const violation = checkConventionViolation(code, filePath, conv);
if (violation) {
violations.push({
convention_id: conv.id,
rule: conv.rule,
severity: violation.severity,
suggestion: violation.suggestion
});
}
}
for (const ap of intelligence2.anti_patterns) {
const violation = checkAntiPatternViolation(code, filePath, ap);
if (violation) {
violations.push({
convention_id: ap.id,
rule: ap.pattern,
severity: violation.severity,
suggestion: violation.suggestion
});
}
}
return {
violations,
passed: violations.filter((v) => v.severity === "error").length === 0
};
}
function checkConventionViolation(code, filePath, conv) {
const rule = conv.rule.toLowerCase();
if (rule.includes("named export") && conv.category === "imports") {
if (code.includes("export default ") && !filePath.includes("page.") && !filePath.includes("layout.")) {
return {
severity: "warning",
suggestion: "Use named exports. Replace `export default X` with `export { X }` or `export const/function X`."
};
}
}
if (rule.includes("path alias") && conv.category === "imports") {
if (code.match(/from\s+['"]\.\.\/\.\.\/\.\.\//)) {
return {
severity: "info",
suggestion: "Use path aliases (e.g., @/) instead of deep relative imports."
};
}
}
if (rule.includes("async/await") || rule.includes("async_await")) {
const thenCount = (code.match(/\.then\s*\(/g) || []).length;
if (thenCount > 0) {
return {
severity: "info",
suggestion: "Use async/await instead of .then() chains."
};
}
}
if (rule.includes("explicit return type") || rule.includes("return types")) {
const functionsWithoutReturnType = code.match(/(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/g) || [];
if (functionsWithoutReturnType.length > 0) {
return {
severity: "info",
suggestion: "Add explicit return type annotations to functions."
};
}
}
return null;
}
function checkAntiPatternViolation(code, filePath, ap) {
const pattern = ap.pattern.toLowerCase();
if (pattern.includes("console.log") || pattern.includes("console statement")) {
const matches = code.match(/console\.(log|debug|info)\s*\(/g) || [];
if (matches.length > 0 && !filePath.includes(".test.") && !filePath.includes(".spec.")) {
return {
severity: ap.severity === "critical" ? "error" : "warning",
suggestion: ap.correct_approach || "Remove console statements or use a proper logger."
};
}
}
if (pattern.includes("any type") || pattern.includes("typescript any")) {
if (code.match(/:\s*any\b/) && !code.includes("eslint-disable")) {
return {
severity: "warning",
suggestion: ap.correct_approach || "Use specific types instead of `any`."
};
}
}
if (pattern.includes("todo") && pattern.includes("comment")) {
if (code.match(/\/\/\s*TODO/i)) {
return {
severity: "info",
suggestion: ap.correct_approach || "Resolve TODO comments before submitting."
};
}
}
return null;
}
// src/tools/get-knowledge.ts
function handleGetKnowledge(intelligence2, input) {
const topic = input.topic.toLowerCase();
const exactMatch = Object.entries(intelligence2.knowledge_graph).find(([key]) => key.toLowerCase() === topic);
if (exactMatch) {
return formatKnowledge(exactMatch[0], exactMatch[1]);
}
const fuzzyMatches = Object.entries(intelligence2.knowledge_graph).filter(
([key]) => key.toLowerCase().includes(topic) || topic.includes(key.toLowerCase())
);
if (fuzzyMatches.length > 0) {
const sections = fuzzyMatches.map(([key, entries]) => formatKnowledge(key, entries));
return sections.join("\n\n");
}
const availableTopics = Object.keys(intelligence2.knowledge_graph);
if (availableTopics.length === 0) {
return "No domain knowledge available yet. Run `codehabits sync` to analyze PRs and extract knowledge.";
}
return `No knowledge found for topic: "${input.topic}"
Available topics: ${availableTopics.join(", ")}`;
}
function formatKnowledge(topic, entries) {
const lines = [`## ${topic}`, ""];
for (const [key, entry] of Object.entries(entries)) {
lines.push(`### ${key}`);
lines.push(entry.value);
lines.push(`(confidence: ${Math.round(entry.confidence * 100)}%, sources: ${entry.source_prs.length} PRs)`);
lines.push("");
}
return lines.join("\n");
}
// src/tools/suggest-reviewers.ts
function handleSuggestReviewers(intelligence2, input) {
if (intelligence2.reviewers.length === 0) {
return [];
}
const changedDirs = /* @__PURE__ */ new Set();
for (const file of input.files) {
const parts = file.split("/");
if (parts.length > 1) {
changedDirs.add(parts.slice(0, 2).join("/"));
changedDirs.add(parts[0]);
} else {
changedDirs.add(parts[0]);
}
}
const scored = [];
for (const reviewer of intelligence2.reviewers) {
let relevance = 0;
const matchedAreas = [];
for (const expertise of reviewer.expertise) {
const area = expertise.area.toLowerCase();
for (const dir of changedDirs) {
const dirLower = dir.toLowerCase();
if (area.includes(dirLower) || dirLower.includes(area)) {
relevance += expertise.confidence * expertise.review_count;
matchedAreas.push(expertise.area);
}
}
for (const file of input.files) {
const ext = file.split(".").pop()?.toLowerCase() || "";
if (area.includes(ext) || area.includes(file.split("/").pop() || "")) {
relevance += expertise.confidence * 0.5;
if (!matchedAreas.includes(expertise.area)) {
matchedAreas.push(expertise.area);
}
}
}
}
if (relevance > 0) {
scored.push({
login: reviewer.github_login,
relevance: Math.round(relevance * 100) / 100,
areas: [...new Set(matchedAreas)]
});
}
}
return scored.sort((a, b) => b.relevance - a.relevance).slice(0, 5);
}
// src/tools/record-feedback.ts
import { recordProposal } from "@codehabits/cli/mcp-proposals";
function handleRecordFeedback(cwd, input) {
const result = recordProposal(cwd, input);
if (!result.ok) {
return { text: `Error: ${result.error}` };
}
return {
text: `Recorded proposal ${result.id}. It is pending in .codehabits/proposals.json. Use approve_proposal to apply it to tracked intelligence.`
};
}
// src/tools/approve-proposal.ts
import { approveProposal } from "@codehabits/cli/mcp-proposals";
async function handleApproveProposal(cwd, proposalId) {
const result = await approveProposal(cwd, proposalId);
if (!result.ok) {
return { text: `Error: ${result.error}` };
}
return {
text: `Proposal ${proposalId} approved. conventions.json and generated skill files were updated. Commit tracked .codehabits/, .claude/skills/, .agents/skills/, and AGENTS.md when ready.`
};
}
// src/server.ts
var intelligence = null;
async function startServer() {
const cwd = process.cwd();
intelligence = readIntelligenceFile(cwd);
if (!intelligence) {
console.error("Warning: No .codehabits/ intelligence files found in current directory.");
console.error("Run `codehabits enable` to generate intelligence first.");
}
const unwatch = watchIntelligenceFile(cwd, (updated) => {
intelligence = updated;
if (updated) {
console.error("Intelligence file updated, reloaded.");
}
});
const server = new McpServer({
name: "codehabits",
version: "0.1.0"
});
server.tool(
"get_team_context",
"Get team coding conventions, anti-patterns, and domain knowledge. Use this before writing or reviewing code to understand how the team works.",
{
scope: z4.enum(["all", "imports", "testing", "naming", "async", "api", "auth", "database", "structure", "error-handling", "documentation", "security", "performance", "type-safety"]).optional().describe("Filter context to a specific area"),
file_path: z4.string().optional().describe("File path to get context specific to that file type")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleGetTeamContext(intelligence, {
scope: params.scope,
file_path: params.file_path
});
return {
content: [{ type: "text", text: result }]
};
}
);
server.tool(
"check_code",
"Validate a code snippet against team conventions and anti-patterns. Returns violations with suggestions.",
{
code: z4.string().describe("The code snippet to check"),
file_path: z4.string().optional().describe("File path for context-aware checking")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleCheckCode(intelligence, {
code: params.code,
file_path: params.file_path
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
}
);
server.tool(
"get_knowledge",
"Get domain-specific knowledge about a topic (e.g., authentication, database, deployment)",
{
topic: z4.string().describe('The topic to look up (e.g., "authentication", "database", "api")')
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleGetKnowledge(intelligence, { topic: params.topic });
return {
content: [{ type: "text", text: result }]
};
}
);
server.tool(
"suggest_reviewers",
"Suggest the best reviewers for a set of changed files based on team expertise",
{
files: z4.array(z4.string()).describe("List of file paths that were changed")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleSuggestReviewers(intelligence, { files: params.files });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
}
);
server.tool(
"record_feedback",
"Record when a user overrides or corrects team intelligence, or propose a new convention. Writes to local .codehabits/proposals.json (gitignored); does not change tracked intelligence until approve_proposal.",
{
type: z4.enum(["new_convention", "update_convention", "new_exception", "deprecate"]).describe("Kind of change"),
targetId: z4.string().optional().describe("ID of existing convention (required for update, exception, or deprecate)"),
rule: z4.string().optional().describe("Convention rule text (for new conventions)"),
reason: z4.string().describe("Why this change is being proposed"),
category: z4.string().optional().describe("Convention category (e.g. testing, naming)"),
file_path: z4.string().optional().describe("File path where this came up")
},
async (params) => {
const result = handleRecordFeedback(cwd, {
type: params.type,
targetId: params.targetId,
rule: params.rule,
reason: params.reason,
category: params.category,
filePath: params.file_path
});
return {
content: [{ type: "text", text: result.text }]
};
}
);
server.tool(
"approve_proposal",
"Approve a pending proposal and merge it into active intelligence (conventions.json) and regenerate skill markdown. Creates tracked git changes.",
{
proposal_id: z4.string().describe("Proposal id from record_feedback (e.g. prop-001)")
},
async (params) => {
const result = await handleApproveProposal(cwd, params.proposal_id);
return {
content: [{ type: "text", text: result.text }]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
process.on("SIGINT", () => {
unwatch();
process.exit(0);
});
process.on("SIGTERM", () => {
unwatch();
process.exit(0);
});
}
export {
startServer
};
# @codehabits/mcp
[![npm version](https://img.shields.io/npm/v/@codehabits/mcp.svg)](https://www.npmjs.com/package/@codehabits/mcp)
[![license](https://img.shields.io/npm/l/@codehabits/mcp.svg)](https://github.com/codehabits-dev/codehabits/blob/main/LICENSE)
**MCP server for codehabits team intelligence.** Expose your repository’s conventions, anti-patterns, and domain knowledge to AI coding agents through the [Model Context Protocol](https://modelcontextprotocol.io).
Works with **Cursor**, **Claude Code**, and other MCP-capable clients. Pair with [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli) to generate `.codehabits/` from PR history.
- **Website:** [codehabits.dev](https://codehabits.dev)
- **Docs:** [codehabits.dev/docs](https://codehabits.dev/docs)
- **CLI (generate intelligence):** [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli)
---
## Prerequisites
1. **Node.js 20+**
2. Intelligence files in the project root (run once per repo):
```bash
npx @codehabits/cli login
npx @codehabits/cli enable
```
The MCP server reads `.codehabits/` from the process **working directory** (`cwd`). It reloads when intelligence files change on disk.
---
## Quick install (Cursor)
From your repository root (after `enable`):
```bash
npx @codehabits/cli mcp-install
```
This writes `.cursor/mcp.json` (or `~/.cursor/mcp.json` with `--global`):
```json
{
"mcpServers": {
"codehabits": {
"command": "npx",
"args": ["@codehabits/mcp"],
"cwd": "/absolute/path/to/your/repo"
}
}
}
```
Restart the IDE so the server loads.
---
## Manual MCP configuration
Use the same shape anywhere MCP stdio servers are supported. **Important:** set `cwd` to the repo that contains `.codehabits/`.
### Cursor (project)
Path: `.cursor/mcp.json`
```json
{
"mcpServers": {
"codehabits": {
"command": "npx",
"args": ["-y", "@codehabits/mcp"],
"cwd": "${workspaceFolder}"
}
}
}
```
### Claude Code (example)
Add to your Claude MCP config (path varies by install):
```json
{
"mcpServers": {
"codehabits": {
"command": "npx",
"args": ["-y", "@codehabits/mcp"],
"cwd": "/path/to/your/repo"
}
}
}
```
### Run directly (debug)
```bash
cd /path/to/your/repo
npx @codehabits/mcp
```
Stdio transport only; intended for IDE integration, not HTTP.
---
## Tools
| Tool | Description |
| --- | --- |
| `get_team_context` | Conventions, anti-patterns, and knowledge; optionally filtered by `scope` or `file_path` |
| `check_code` | Validate a code snippet against team rules; returns violations and suggestions |
| `get_knowledge` | Look up a domain topic (e.g. `authentication`, `database`, `deployment`) |
| `suggest_reviewers` | Rank reviewers for a list of changed file paths |
| `record_feedback` | Propose a convention change or correction (writes local `proposals.json`, gitignored) |
| `approve_proposal` | Merge an approved proposal into tracked intelligence and regenerate skills |
### `get_team_context`
Use **before** writing or reviewing code.
| Parameter | Type | Description |
| --- | --- | --- |
| `scope` | enum (optional) | `all`, `imports`, `testing`, `naming`, `async`, `api`, `auth`, `database`, `structure`, `error-handling`, `documentation`, `security`, `performance`, `type-safety` |
| `file_path` | string (optional) | Tailor context to a file type or path |
### `check_code`
| Parameter | Type | Description |
| --- | --- | --- |
| `code` | string | Snippet to validate |
| `file_path` | string (optional) | Path for context-aware rules |
### `get_knowledge`
| Parameter | Type | Description |
| --- | --- | --- |
| `topic` | string | Topic name (fuzzy match supported) |
### `suggest_reviewers`
| Parameter | Type | Description |
| --- | --- | --- |
| `files` | string[] | Changed file paths |
### `record_feedback` / `approve_proposal`
Agents can suggest updates when the user overrides team rules:
1. `record_feedback` stores a proposal in `.codehabits/proposals.json` (not committed by default).
2. Human reviews, then `approve_proposal` with `proposal_id` updates `conventions.json` and regenerates skill markdown / `AGENTS.md` (creates normal git changes to commit).
| `record_feedback` field | Description |
| --- | --- |
| `type` | `new_convention`, `update_convention`, `new_exception`, `deprecate` |
| `targetId` | Existing convention id (for update/exception/deprecate) |
| `rule` | Rule text (new conventions) |
| `reason` | Why the change is proposed |
| `category` | e.g. `testing`, `naming` |
| `file_path` | Where the issue appeared |
---
## Example agent workflows
**Before implementing a feature**
> Call `get_team_context` with `scope: "api"` or `file_path` set to the file you are editing.
**During review**
> Call `check_code` on the diff snippet; fix violations the tool reports.
**When the model disagrees with a convention**
> Call `record_feedback` with `type: "new_exception"` and a clear `reason`; a teammate runs `approve_proposal` after review.
**PR assignment**
> Call `suggest_reviewers` with the list of changed paths.
---
## How it works
```mermaid
sequenceDiagram
participant IDE as IDE / Agent
participant MCP as @codehabits/mcp
participant FS as .codehabits/*.json
IDE->>MCP: tools/call (stdio)
MCP->>FS: read + watch
FS-->>MCP: IntelligenceFile
MCP-->>IDE: conventions / violations / knowledge
```
- **Transport:** MCP over stdio (`@modelcontextprotocol/sdk`).
- **State:** Loaded from disk; no separate database.
- **Hot reload:** File watcher updates in-memory intelligence when JSON changes (e.g. after `codehabits sync`).
If intelligence is missing, tools return a message asking you to run `codehabits enable` first.
---
## Passive vs active intelligence
| Mechanism | Package | When agents use it |
| --- | --- | --- |
| `AGENTS.md` + `.claude/skills/` + `.agents/skills/` | CLI `enable` | Automatically in context / skill discovery |
| MCP tools (this package) | `@codehabits/mcp` | On demand during chat or agent loops |
Use both: skills for always-on team voice; MCP for lookups, checks, and structured feedback.
---
## Troubleshooting
| Symptom | Fix |
| --- | --- |
| “No intelligence data available” | Run `npx @codehabits/cli enable` in the repo; confirm `.codehabits/meta.json` exists |
| Tools return stale rules | Run `npx @codehabits/cli sync`; restart IDE if the watcher did not fire |
| Server starts but wrong conventions | Check MCP `cwd` points at the repo root, not a subfolder |
| `npx` fails in IDE | Use full path to `codehabits-mcp` or pin version: `npx -y @codehabits/mcp@0.1.1` |
Verify integration:
```bash
npx @codehabits/cli status
```
---
## Related packages
| Package | Role |
| --- | --- |
| [@codehabits/cli](https://www.npmjs.com/package/@codehabits/cli) | Login, enable, sync, `mcp-install`, CI tokens |
---
## License
MIT © [codehabits](https://codehabits.dev)
+1
-1
#!/usr/bin/env node
import {
startServer
} from "../chunk-AXQMVMBH.js";
} from "../chunk-KUSOV7Y3.js";

@@ -6,0 +6,0 @@ // bin/codehabits-mcp.ts

import {
startServer
} from "../chunk-AXQMVMBH.js";
} from "../chunk-KUSOV7Y3.js";
export {
startServer
};
{
"name": "@codehabits/mcp",
"version": "0.1.1",
"description": "MCP server for Codehabits team intelligence. Serves team conventions, anti-patterns, and domain knowledge to AI coding agents via the Model Context Protocol.",
"version": "0.1.2",
"description": "MCP server for codehabits: get_team_context, check_code, get_knowledge, and reviewer tools over stdio for Cursor, Claude Code, and MCP-compatible agents.",
"type": "module",

@@ -18,5 +18,10 @@ "license": "MIT",

"cursor",
"claude-code",
"copilot",
"vscode",
"conventions",
"code-review",
"team-intelligence"
"agent-skills",
"team-intelligence",
"codehabits"
],

@@ -30,3 +35,4 @@ "engines": {

"files": [
"dist"
"dist",
"README.md"
],

@@ -36,3 +42,3 @@ "dependencies": {

"zod": "^4.3.6",
"@codehabits/cli": "0.2.2"
"@codehabits/cli": "0.2.3"
},

@@ -39,0 +45,0 @@ "devDependencies": {

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z as z3 } from "zod";
// src/intelligence/reader.ts
import { readFileSync, existsSync, watchFile, unwatchFile } from "fs";
import { join } from "path";
// ../shared/src/schemas/intelligence.ts
import { z } from "zod";
var intelligenceSourceSchema = z.enum([
"code_analysis",
"config_import",
"stack_baseline",
"git_history",
"pr_analysis",
"rejection_analysis",
"comment_analysis",
"agent_proposal"
]);
var maturityLevelSchema = z.union([
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4)
]);
var maturityLabelSchema = z.enum([
"baseline",
"growing",
"established",
"mature"
]);
var conventionCategorySchema = z.enum([
"imports",
"testing",
"naming",
"async",
"structure",
"error-handling",
"api",
"documentation",
"security",
"performance",
"type-safety",
"other"
]);
var antiPatternSeveritySchema = z.enum([
"critical",
"high",
"medium",
"low"
]);
var techStackSchema = z.object({
language: z.string(),
languages: z.array(z.string()).optional(),
framework: z.string().optional(),
framework_version: z.string().optional(),
test_framework: z.string().optional(),
linter: z.string().optional(),
formatter: z.string().optional(),
build_tool: z.string().optional(),
package_manager: z.string().optional()
});
var conventionExampleSchema = z.object({
pr_number: z.number().optional(),
file_path: z.string(),
snippet: z.string().optional()
});
var conventionSchema = z.object({
id: z.string(),
category: conventionCategorySchema,
rule: z.string(),
confidence: z.number().min(0).max(1),
source: intelligenceSourceSchema,
evidence_count: z.number(),
first_seen: z.string(),
examples: z.array(conventionExampleSchema),
metadata: z.record(z.string(), z.unknown()).optional()
});
var antiPatternEvidenceTypeSchema = z.enum([
"rejected_pr",
"review_comment",
"bug_fix"
]);
function normalizeAntiPatternEvidenceType(input) {
if (input === null || input === void 0 || input === "") {
return "review_comment";
}
const s = String(input).trim().toLowerCase().replace(/-/g, "_").replace(/\s+/g, "_");
if (s === "rejected_pr" || s === "rejection" || s === "pr_rejection" || s === "rejected" || s.includes("reject")) {
return "rejected_pr";
}
if (s === "bug_fix" || s === "bugfix" || s === "fix" || s.includes("bug") && s.includes("fix")) {
return "bug_fix";
}
if (s === "review_comment" || s === "comment" || s === "pr_comment" || s === "inline_comment" || s === "discussion" || s === "review" || s.includes("comment")) {
return "review_comment";
}
if (s === "merged_pr" || s === "merged" || s === "pr") {
return "review_comment";
}
return "review_comment";
}
var antiPatternEvidenceSchema = z.object({
pr_number: z.coerce.number(),
type: z.preprocess(
(raw) => normalizeAntiPatternEvidenceType(raw),
antiPatternEvidenceTypeSchema
),
excerpt: z.string().optional()
});
var antiPatternSchema = z.object({
id: z.string(),
pattern: z.string(),
severity: antiPatternSeveritySchema,
reason: z.string(),
correct_approach: z.string(),
evidence: z.array(antiPatternEvidenceSchema)
});
var knowledgeEntrySchema = z.object({
value: z.string(),
confidence: z.number().min(0).max(1),
source_prs: z.array(z.number())
});
var knowledgeGraphSchema = z.record(
z.string(),
z.record(z.string(), knowledgeEntrySchema)
);
var reviewerExpertiseSchema = z.object({
area: z.string(),
confidence: z.number().min(0).max(1),
review_count: z.number()
});
var reviewerStatsSchema = z.object({
total_reviews: z.number(),
avg_review_time_hours: z.number(),
approval_rate: z.number().min(0).max(1)
});
var reviewerProfileSchema = z.object({
github_login: z.string(),
expertise: z.array(reviewerExpertiseSchema),
stats: reviewerStatsSchema
});
var riskFactorSchema = z.object({
type: z.enum(["file", "size", "timing", "complexity"]),
description: z.string(),
value: z.string(),
risk_multiplier: z.number(),
evidence: z.object({
total_prs: z.number(),
incidents: z.number()
})
});
var maturitySchema = z.object({
level: maturityLevelSchema,
label: maturityLabelSchema,
prs_analyzed: z.number(),
next_level_at: z.number(),
sources: z.array(intelligenceSourceSchema)
});
var repositoryInfoSchema = z.object({
owner: z.string(),
name: z.string(),
full_name: z.string(),
analyzed_prs: z.number(),
analyzed_period: z.object({
from: z.string(),
to: z.string()
}),
tech_stack: techStackSchema,
codehabits_version: z.string()
});
var intelligenceFileSchema = z.object({
version: z.string(),
generated_at: z.string(),
last_synced_at: z.string(),
maturity: maturitySchema,
repository: repositoryInfoSchema,
conventions: z.array(conventionSchema),
anti_patterns: z.array(antiPatternSchema),
knowledge_graph: knowledgeGraphSchema,
reviewers: z.array(reviewerProfileSchema),
risk_factors: z.array(riskFactorSchema)
});
var conventionsFileSchema = z.array(conventionSchema);
var antiPatternsFileSchema = z.array(antiPatternSchema);
var knowledgeFileSchema = knowledgeGraphSchema;
var reviewersFileSchema = z.array(reviewerProfileSchema);
var intelligenceMetaSchema = z.object({
version: z.string(),
generated_at: z.string(),
last_synced_at: z.string(),
maturity: maturitySchema,
repository: repositoryInfoSchema,
risk_factors: z.array(riskFactorSchema),
counts: z.object({
conventions: z.number(),
anti_patterns: z.number(),
reviewers: z.number(),
knowledge_topics: z.number()
})
});
var proposalSchema = z.object({
id: z.string(),
type: z.enum(["new_convention", "update_convention", "new_exception", "deprecate"]),
targetId: z.string().optional(),
data: conventionSchema.partial(),
reason: z.string(),
filePath: z.string().optional(),
createdAt: z.string(),
status: z.enum(["pending", "approved", "rejected"]),
approvedBy: z.string().optional()
});
var proposalsFileSchema = z.array(proposalSchema);
// ../shared/src/schemas/config.ts
import { z as z2 } from "zod";
var customRuleSchema = z2.object({
name: z2.string(),
severity: z2.enum(["error", "warning", "info"]),
description: z2.string()
});
var CodehabitsConfigSchema = z2.object({
version: z2.string(),
analysis: z2.object({
lookback_months: z2.number().default(12),
min_prs_for_pattern: z2.number().default(10),
min_confidence: z2.number().min(0).max(1).default(0.7),
include_closed_prs: z2.boolean().default(true)
}),
ignore: z2.object({
files: z2.array(z2.string()).default([]),
authors: z2.array(z2.string()).default(["dependabot", "renovate"]),
labels: z2.array(z2.string()).default(["wip", "draft"])
}),
sync: z2.object({
auto_commit: z2.boolean().default(false)
}),
custom_rules: z2.array(customRuleSchema).default([])
});
var userConfigSchema = z2.object({
auth: z2.object({
token: z2.string(),
github_token: z2.string(),
user: z2.object({
id: z2.string(),
login: z2.string(),
email: z2.string()
})
}).optional(),
defaults: z2.object({
ai_model: z2.string().default("deepseek")
}).optional()
});
// ../shared/src/constants.ts
var PATHS = {
/** @deprecated Use individual file paths instead */
INTELLIGENCE_FILE: ".codehabits/intelligence.json",
CONVENTIONS_FILE: ".codehabits/conventions.json",
ANTI_PATTERNS_FILE: ".codehabits/anti-patterns.json",
KNOWLEDGE_FILE: ".codehabits/knowledge.json",
REVIEWERS_FILE: ".codehabits/reviewers.json",
META_FILE: ".codehabits/meta.json",
PROPOSALS_FILE: ".codehabits/proposals.json",
README_FILE: ".codehabits/README.md",
CODEHABITS_GITIGNORE: ".codehabits/.gitignore",
CONFIG_FILE: ".codehabits/config.json",
SKILL_DIR: ".cursor/skills/codehabits-team-intel",
SKILL_FILE: ".cursor/skills/codehabits-team-intel/SKILL.md",
SKILL_REFERENCES: ".cursor/skills/codehabits-team-intel/references",
SKILL_SCRIPTS: ".cursor/skills/codehabits-team-intel/scripts",
USER_CONFIG: ".codehabits/config.json"
};
var AI_MODELS = {
FREE: "deepseek/deepseek-v3.2",
PAID: "anthropic/claude-sonnet-4-20250514"
};
var PLAN_LIMITS = {
free: {
private_repos: 1,
pr_lookback: 100,
ai_model: AI_MODELS.FREE
},
team: {
private_repos: Infinity,
pr_lookback: Infinity,
ai_model: AI_MODELS.PAID
},
enterprise: {
private_repos: Infinity,
pr_lookback: Infinity,
ai_model: AI_MODELS.PAID
}
};
// src/intelligence/reader.ts
var SPLIT_FILES = [
PATHS.META_FILE,
PATHS.CONVENTIONS_FILE,
PATHS.ANTI_PATTERNS_FILE,
PATHS.KNOWLEDGE_FILE,
PATHS.REVIEWERS_FILE
];
function readIntelligenceFile(cwd) {
const metaPath = join(cwd, PATHS.META_FILE);
if (existsSync(metaPath)) {
return readSplitFiles(cwd);
}
return readLegacyFile(cwd);
}
function watchIntelligenceFile(cwd, onChange) {
const reload = () => {
onChange(readIntelligenceFile(cwd));
};
const allPaths = [
join(cwd, PATHS.INTELLIGENCE_FILE),
...SPLIT_FILES.map((rel) => join(cwd, rel))
];
for (const abs of allPaths) {
watchFile(abs, { interval: 2e3 }, reload);
}
return () => {
for (const abs of allPaths) {
unwatchFile(abs, reload);
}
};
}
function readSplitFiles(cwd) {
try {
const meta = readJson(join(cwd, PATHS.META_FILE), intelligenceMetaSchema);
if (!meta) return null;
const conventions = readJson(join(cwd, PATHS.CONVENTIONS_FILE), conventionsFileSchema) ?? [];
const antiPatterns = readJson(join(cwd, PATHS.ANTI_PATTERNS_FILE), antiPatternsFileSchema) ?? [];
const knowledgeGraph = readJson(join(cwd, PATHS.KNOWLEDGE_FILE), knowledgeFileSchema) ?? {};
const reviewers = readJson(join(cwd, PATHS.REVIEWERS_FILE), reviewersFileSchema) ?? [];
return {
version: meta.version,
generated_at: meta.generated_at,
last_synced_at: meta.last_synced_at,
maturity: meta.maturity,
repository: meta.repository,
conventions,
anti_patterns: antiPatterns,
knowledge_graph: knowledgeGraph,
reviewers,
risk_factors: meta.risk_factors
};
} catch (error) {
console.error("Failed to read split intelligence files:", error);
return null;
}
}
function readLegacyFile(cwd) {
return readJson(
join(cwd, PATHS.INTELLIGENCE_FILE),
intelligenceFileSchema
);
}
function readJson(filePath, schema) {
if (!existsSync(filePath)) return null;
try {
const raw = readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
return schema.parse(parsed);
} catch (error) {
console.error(`Failed to read ${filePath}:`, error);
return null;
}
}
// src/tools/get-team-context.ts
function handleGetTeamContext(intelligence2, input) {
const scope = input.scope || "all";
const sections = [];
sections.push(`# Team Intelligence: ${intelligence2.repository.full_name}`);
sections.push(
`Maturity: ${intelligence2.maturity.label} | PRs analyzed: ${intelligence2.maturity.prs_analyzed}`
);
sections.push(
`Tech stack: ${intelligence2.repository.tech_stack.language}${intelligence2.repository.tech_stack.framework ? ` / ${intelligence2.repository.tech_stack.framework}` : ""}`
);
sections.push("");
let conventions;
if (scope === "all") {
conventions = intelligence2.conventions.slice(0, 20);
} else {
conventions = intelligence2.conventions.filter((c) => {
if (c.category === scope) return true;
if (input.file_path) {
return isConventionRelevantToFile(c, input.file_path);
}
return false;
});
if (conventions.length < 3) {
conventions = [
...conventions,
...intelligence2.conventions.filter((c) => c.category === scope).slice(0, 5)
];
}
}
if (conventions.length > 0) {
sections.push("## Conventions");
sections.push("");
for (const conv of conventions) {
sections.push(
`- **${conv.rule}** (${conv.category}, ${Math.round(conv.confidence * 100)}% confidence)`
);
}
sections.push("");
}
let antiPatterns;
if (scope === "all") {
antiPatterns = intelligence2.anti_patterns.slice(0, 10);
} else {
antiPatterns = intelligence2.anti_patterns.filter((ap) => {
const patternLower = ap.pattern.toLowerCase();
const scopeLower = scope.toLowerCase();
return patternLower.includes(scopeLower);
});
if (antiPatterns.length === 0) {
antiPatterns = intelligence2.anti_patterns.slice(0, 5);
}
}
if (antiPatterns.length > 0) {
sections.push("## Anti-Patterns (Avoid These)");
sections.push("");
for (const ap of antiPatterns) {
sections.push(
`- **${ap.pattern}** [${ap.severity}] - ${ap.correct_approach}`
);
}
sections.push("");
}
if (scope !== "all") {
const relevantTopics = Object.entries(intelligence2.knowledge_graph).filter(
([topic]) => topic.toLowerCase().includes(scope.toLowerCase())
);
if (relevantTopics.length > 0) {
sections.push("## Domain Knowledge");
sections.push("");
for (const [topic, entries] of relevantTopics) {
sections.push(`### ${topic}`);
for (const [key, entry] of Object.entries(entries)) {
sections.push(`- ${key}: ${entry.value}`);
}
}
sections.push("");
}
}
if (input.file_path) {
const fileReviewers = intelligence2.reviewers.filter(
(r) => r.expertise.some(
(e) => e.area.split("/").some((part) => input.file_path.includes(part))
)
).slice(0, 3);
if (fileReviewers.length > 0) {
sections.push("## Suggested Reviewers for This File");
sections.push("");
for (const r of fileReviewers) {
sections.push(
`- @${r.github_login} (${r.stats.total_reviews} reviews)`
);
}
}
}
return sections.join("\n");
}
function isConventionRelevantToFile(conv, filePath) {
const fp = filePath.toLowerCase();
if (fp.includes(".test.") || fp.includes(".spec.") || fp.includes("__tests__")) {
return conv.category === "testing";
}
if (fp.includes("/api/") || fp.includes("route.ts") || fp.includes("handler")) {
return conv.category === "api" || conv.category === "error-handling";
}
if (fp.includes("component") || fp.endsWith(".tsx")) {
return conv.category === "naming" || conv.category === "structure";
}
return false;
}
// src/tools/check-code.ts
function handleCheckCode(intelligence2, input) {
const violations = [];
const code = input.code;
const filePath = input.file_path || "";
for (const conv of intelligence2.conventions) {
if (conv.confidence < 0.7) continue;
const violation = checkConventionViolation(code, filePath, conv);
if (violation) {
violations.push({
convention_id: conv.id,
rule: conv.rule,
severity: violation.severity,
suggestion: violation.suggestion
});
}
}
for (const ap of intelligence2.anti_patterns) {
const violation = checkAntiPatternViolation(code, filePath, ap);
if (violation) {
violations.push({
convention_id: ap.id,
rule: ap.pattern,
severity: violation.severity,
suggestion: violation.suggestion
});
}
}
return {
violations,
passed: violations.filter((v) => v.severity === "error").length === 0
};
}
function checkConventionViolation(code, filePath, conv) {
const rule = conv.rule.toLowerCase();
if (rule.includes("named export") && conv.category === "imports") {
if (code.includes("export default ") && !filePath.includes("page.") && !filePath.includes("layout.")) {
return {
severity: "warning",
suggestion: "Use named exports. Replace `export default X` with `export { X }` or `export const/function X`."
};
}
}
if (rule.includes("path alias") && conv.category === "imports") {
if (code.match(/from\s+['"]\.\.\/\.\.\/\.\.\//)) {
return {
severity: "info",
suggestion: "Use path aliases (e.g., @/) instead of deep relative imports."
};
}
}
if (rule.includes("async/await") || rule.includes("async_await")) {
const thenCount = (code.match(/\.then\s*\(/g) || []).length;
if (thenCount > 0) {
return {
severity: "info",
suggestion: "Use async/await instead of .then() chains."
};
}
}
if (rule.includes("explicit return type") || rule.includes("return types")) {
const functionsWithoutReturnType = code.match(/(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/g) || [];
if (functionsWithoutReturnType.length > 0) {
return {
severity: "info",
suggestion: "Add explicit return type annotations to functions."
};
}
}
return null;
}
function checkAntiPatternViolation(code, filePath, ap) {
const pattern = ap.pattern.toLowerCase();
if (pattern.includes("console.log") || pattern.includes("console statement")) {
const matches = code.match(/console\.(log|debug|info)\s*\(/g) || [];
if (matches.length > 0 && !filePath.includes(".test.") && !filePath.includes(".spec.")) {
return {
severity: ap.severity === "critical" ? "error" : "warning",
suggestion: ap.correct_approach || "Remove console statements or use a proper logger."
};
}
}
if (pattern.includes("any type") || pattern.includes("typescript any")) {
if (code.match(/:\s*any\b/) && !code.includes("eslint-disable")) {
return {
severity: "warning",
suggestion: ap.correct_approach || "Use specific types instead of `any`."
};
}
}
if (pattern.includes("todo") && pattern.includes("comment")) {
if (code.match(/\/\/\s*TODO/i)) {
return {
severity: "info",
suggestion: ap.correct_approach || "Resolve TODO comments before submitting."
};
}
}
return null;
}
// src/tools/get-knowledge.ts
function handleGetKnowledge(intelligence2, input) {
const topic = input.topic.toLowerCase();
const exactMatch = Object.entries(intelligence2.knowledge_graph).find(([key]) => key.toLowerCase() === topic);
if (exactMatch) {
return formatKnowledge(exactMatch[0], exactMatch[1]);
}
const fuzzyMatches = Object.entries(intelligence2.knowledge_graph).filter(
([key]) => key.toLowerCase().includes(topic) || topic.includes(key.toLowerCase())
);
if (fuzzyMatches.length > 0) {
const sections = fuzzyMatches.map(([key, entries]) => formatKnowledge(key, entries));
return sections.join("\n\n");
}
const availableTopics = Object.keys(intelligence2.knowledge_graph);
if (availableTopics.length === 0) {
return "No domain knowledge available yet. Run `codehabits sync` to analyze PRs and extract knowledge.";
}
return `No knowledge found for topic: "${input.topic}"
Available topics: ${availableTopics.join(", ")}`;
}
function formatKnowledge(topic, entries) {
const lines = [`## ${topic}`, ""];
for (const [key, entry] of Object.entries(entries)) {
lines.push(`### ${key}`);
lines.push(entry.value);
lines.push(`(confidence: ${Math.round(entry.confidence * 100)}%, sources: ${entry.source_prs.length} PRs)`);
lines.push("");
}
return lines.join("\n");
}
// src/tools/suggest-reviewers.ts
function handleSuggestReviewers(intelligence2, input) {
if (intelligence2.reviewers.length === 0) {
return [];
}
const changedDirs = /* @__PURE__ */ new Set();
for (const file of input.files) {
const parts = file.split("/");
if (parts.length > 1) {
changedDirs.add(parts.slice(0, 2).join("/"));
changedDirs.add(parts[0]);
} else {
changedDirs.add(parts[0]);
}
}
const scored = [];
for (const reviewer of intelligence2.reviewers) {
let relevance = 0;
const matchedAreas = [];
for (const expertise of reviewer.expertise) {
const area = expertise.area.toLowerCase();
for (const dir of changedDirs) {
const dirLower = dir.toLowerCase();
if (area.includes(dirLower) || dirLower.includes(area)) {
relevance += expertise.confidence * expertise.review_count;
matchedAreas.push(expertise.area);
}
}
for (const file of input.files) {
const ext = file.split(".").pop()?.toLowerCase() || "";
if (area.includes(ext) || area.includes(file.split("/").pop() || "")) {
relevance += expertise.confidence * 0.5;
if (!matchedAreas.includes(expertise.area)) {
matchedAreas.push(expertise.area);
}
}
}
}
if (relevance > 0) {
scored.push({
login: reviewer.github_login,
relevance: Math.round(relevance * 100) / 100,
areas: [...new Set(matchedAreas)]
});
}
}
return scored.sort((a, b) => b.relevance - a.relevance).slice(0, 5);
}
// src/tools/record-feedback.ts
import { recordProposal } from "@codehabits/cli/mcp-proposals";
function handleRecordFeedback(cwd, input) {
const result = recordProposal(cwd, input);
if (!result.ok) {
return { text: `Error: ${result.error}` };
}
return {
text: `Recorded proposal ${result.id}. It is pending in .codehabits/proposals.json. Use approve_proposal to apply it to tracked intelligence.`
};
}
// src/tools/approve-proposal.ts
import { approveProposal } from "@codehabits/cli/mcp-proposals";
async function handleApproveProposal(cwd, proposalId) {
const result = await approveProposal(cwd, proposalId);
if (!result.ok) {
return { text: `Error: ${result.error}` };
}
return {
text: `Proposal ${proposalId} approved. conventions.json and generated skill files were updated. Commit the tracked .codehabits/ and .cursor/skills/ changes when ready.`
};
}
// src/server.ts
var intelligence = null;
async function startServer() {
const cwd = process.cwd();
intelligence = readIntelligenceFile(cwd);
if (!intelligence) {
console.error("Warning: No .codehabits/ intelligence files found in current directory.");
console.error("Run `codehabits enable` to generate intelligence first.");
}
const unwatch = watchIntelligenceFile(cwd, (updated) => {
intelligence = updated;
if (updated) {
console.error("Intelligence file updated, reloaded.");
}
});
const server = new McpServer({
name: "codehabits",
version: "0.1.0"
});
server.tool(
"get_team_context",
"Get team coding conventions, anti-patterns, and domain knowledge. Use this before writing or reviewing code to understand how the team works.",
{
scope: z3.enum(["all", "imports", "testing", "naming", "async", "api", "auth", "database", "structure", "error-handling", "documentation", "security", "performance", "type-safety"]).optional().describe("Filter context to a specific area"),
file_path: z3.string().optional().describe("File path to get context specific to that file type")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleGetTeamContext(intelligence, {
scope: params.scope,
file_path: params.file_path
});
return {
content: [{ type: "text", text: result }]
};
}
);
server.tool(
"check_code",
"Validate a code snippet against team conventions and anti-patterns. Returns violations with suggestions.",
{
code: z3.string().describe("The code snippet to check"),
file_path: z3.string().optional().describe("File path for context-aware checking")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleCheckCode(intelligence, {
code: params.code,
file_path: params.file_path
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
}
);
server.tool(
"get_knowledge",
"Get domain-specific knowledge about a topic (e.g., authentication, database, deployment)",
{
topic: z3.string().describe('The topic to look up (e.g., "authentication", "database", "api")')
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleGetKnowledge(intelligence, { topic: params.topic });
return {
content: [{ type: "text", text: result }]
};
}
);
server.tool(
"suggest_reviewers",
"Suggest the best reviewers for a set of changed files based on team expertise",
{
files: z3.array(z3.string()).describe("List of file paths that were changed")
},
async (params) => {
if (!intelligence) {
return {
content: [{ type: "text", text: "No intelligence data available. Run `codehabits enable` first." }]
};
}
const result = handleSuggestReviewers(intelligence, { files: params.files });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
}
);
server.tool(
"record_feedback",
"Record when a user overrides or corrects team intelligence, or propose a new convention. Writes to local .codehabits/proposals.json (gitignored); does not change tracked intelligence until approve_proposal.",
{
type: z3.enum(["new_convention", "update_convention", "new_exception", "deprecate"]).describe("Kind of change"),
targetId: z3.string().optional().describe("ID of existing convention (required for update, exception, or deprecate)"),
rule: z3.string().optional().describe("Convention rule text (for new conventions)"),
reason: z3.string().describe("Why this change is being proposed"),
category: z3.string().optional().describe("Convention category (e.g. testing, naming)"),
file_path: z3.string().optional().describe("File path where this came up")
},
async (params) => {
const result = handleRecordFeedback(cwd, {
type: params.type,
targetId: params.targetId,
rule: params.rule,
reason: params.reason,
category: params.category,
filePath: params.file_path
});
return {
content: [{ type: "text", text: result.text }]
};
}
);
server.tool(
"approve_proposal",
"Approve a pending proposal and merge it into active intelligence (conventions.json) and regenerate skill markdown. Creates tracked git changes.",
{
proposal_id: z3.string().describe("Proposal id from record_feedback (e.g. prop-001)")
},
async (params) => {
const result = await handleApproveProposal(cwd, params.proposal_id);
return {
content: [{ type: "text", text: result.text }]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
process.on("SIGINT", () => {
unwatch();
process.exit(0);
});
process.on("SIGTERM", () => {
unwatch();
process.exit(0);
});
}
export {
startServer
};