@splicr/mcp-server 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { grepKnowledgeSchema, handleGrepKnowledge } from './tools/grep-knowledge
15
15
  import { exploreKnowledgeSchema, handleExploreKnowledge } from './tools/explore-knowledge.js';
16
16
  import { getDecisionsSchema, handleGetDecisions } from './tools/get-decisions.js';
17
17
  import { getTeamStatusSchema, handleGetTeamStatus } from './tools/get-team-status.js';
18
+ import { reviewCodeSchema, handleReviewCode } from './tools/review-code.js';
18
19
  import { completeSession } from './lib/api-client.js';
19
20
  // Prevent unhandled errors from crashing the MCP server
20
21
  process.on('uncaughtException', (err) => {
@@ -44,6 +45,9 @@ CONTRACTS (you must follow these):
44
45
  3. Only fall back to web search or training data if Splicr returns no results or results don't cover the question.
45
46
  4. At the END of significant sessions, call save_from_agent with decisions, discoveries, or workarounds you made. Include the context field for attribution.
46
47
 
48
+ QUALITY GATE:
49
+ 5. Before committing or opening a PR, call review_code to check your changes against the team's active patterns. Fix any violations before proceeding.
50
+
47
51
  FAILURE MODES:
48
52
  - Splicr returns no results → proceed with web search or training data
49
53
  - Splicr API is unreachable → proceed without it, don't block the session
@@ -65,6 +69,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
65
69
  exploreKnowledgeSchema,
66
70
  getDecisionsSchema,
67
71
  getTeamStatusSchema,
72
+ reviewCodeSchema,
68
73
  ],
69
74
  }));
70
75
  // Handle tool calls with per-tool timeout
@@ -85,6 +90,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
85
90
  explore_knowledge: handleExploreKnowledge,
86
91
  get_decisions: handleGetDecisions,
87
92
  get_team_status: handleGetTeamStatus,
93
+ review_code: handleReviewCode,
88
94
  }[name];
89
95
  if (!handler) {
90
96
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Pattern Validator - checks git diff against active project patterns.
3
+ * Core logic reusable from MCP tool, hooks, or future GitHub Action.
4
+ */
5
+ export interface PatternViolationReport {
6
+ repo: string;
7
+ project_name: string | null;
8
+ diff_stats: {
9
+ files_changed: number;
10
+ insertions: number;
11
+ deletions: number;
12
+ };
13
+ patterns_checked: Array<{
14
+ name: string;
15
+ description: string;
16
+ }>;
17
+ diff_summary: string;
18
+ review_prompt: string;
19
+ error?: string;
20
+ }
21
+ export declare function validateAgainstPatterns(cwd: string, scope?: 'staged' | 'unstaged' | 'last-commit' | 'branch'): Promise<PatternViolationReport>;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Pattern Validator - checks git diff against active project patterns.
3
+ * Core logic reusable from MCP tool, hooks, or future GitHub Action.
4
+ */
5
+ import { execSync } from 'child_process';
6
+ import { getProjectContext } from './api-client.js';
7
+ import { detectProject } from './project-detector.js';
8
+ /** Get git diff - supports staged, unstaged, or last N commits */
9
+ function getGitDiff(cwd, scope) {
10
+ try {
11
+ const cmds = {
12
+ 'staged': 'git diff --cached',
13
+ 'unstaged': 'git diff',
14
+ 'last-commit': 'git diff HEAD~1 HEAD',
15
+ 'branch': 'git diff main...HEAD',
16
+ };
17
+ return execSync(cmds[scope], { cwd, encoding: 'utf-8', maxBuffer: 1024 * 1024 }).trim();
18
+ }
19
+ catch {
20
+ return '';
21
+ }
22
+ }
23
+ /** Get diff stats */
24
+ function getDiffStats(cwd, scope) {
25
+ try {
26
+ const cmds = {
27
+ 'staged': 'git diff --cached --shortstat',
28
+ 'unstaged': 'git diff --shortstat',
29
+ 'last-commit': 'git diff HEAD~1 HEAD --shortstat',
30
+ 'branch': 'git diff main...HEAD --shortstat',
31
+ };
32
+ const stat = execSync(cmds[scope], { cwd, encoding: 'utf-8' }).trim();
33
+ const files = stat.match(/(\d+) files? changed/)?.[1] || '0';
34
+ const ins = stat.match(/(\d+) insertions?/)?.[1] || '0';
35
+ const del = stat.match(/(\d+) deletions?/)?.[1] || '0';
36
+ return { files_changed: parseInt(files), insertions: parseInt(ins), deletions: parseInt(del) };
37
+ }
38
+ catch {
39
+ return { files_changed: 0, insertions: 0, deletions: 0 };
40
+ }
41
+ }
42
+ /** Get changed file names */
43
+ function getChangedFiles(cwd, scope) {
44
+ try {
45
+ const cmds = {
46
+ 'staged': 'git diff --cached --name-only',
47
+ 'unstaged': 'git diff --name-only',
48
+ 'last-commit': 'git diff HEAD~1 HEAD --name-only',
49
+ 'branch': 'git diff main...HEAD --name-only',
50
+ };
51
+ const result = execSync(cmds[scope], { cwd, encoding: 'utf-8' }).trim();
52
+ return result ? result.split('\n') : [];
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ /** Truncate diff to fit in context - keep most important parts */
59
+ function truncateDiff(diff, maxLines = 200) {
60
+ const lines = diff.split('\n');
61
+ if (lines.length <= maxLines)
62
+ return diff;
63
+ // Keep first maxLines with a truncation note
64
+ return lines.slice(0, maxLines).join('\n') + `\n\n... (truncated - ${lines.length - maxLines} more lines. Use a narrower scope for full diff.)`;
65
+ }
66
+ export async function validateAgainstPatterns(cwd, scope = 'staged') {
67
+ // Detect project
68
+ const detected = await detectProject(cwd).catch(() => null);
69
+ if (!detected) {
70
+ return {
71
+ repo: cwd,
72
+ project_name: null,
73
+ diff_stats: { files_changed: 0, insertions: 0, deletions: 0 },
74
+ patterns_checked: [],
75
+ diff_summary: '',
76
+ review_prompt: '',
77
+ error: 'Could not detect project. Register this project with Splicr first.',
78
+ };
79
+ }
80
+ // Fetch patterns
81
+ let patterns = [];
82
+ try {
83
+ const ctx = await getProjectContext({
84
+ project_name: detected.name,
85
+ project_id: detected.id,
86
+ limit: 1, // only need patterns
87
+ });
88
+ patterns = (ctx.patterns || []).map((p) => ({
89
+ name: p.name,
90
+ description: p.description,
91
+ }));
92
+ }
93
+ catch { /* no patterns available */ }
94
+ if (patterns.length === 0) {
95
+ return {
96
+ repo: detected.name,
97
+ project_name: detected.name,
98
+ diff_stats: { files_changed: 0, insertions: 0, deletions: 0 },
99
+ patterns_checked: [],
100
+ diff_summary: '',
101
+ review_prompt: '',
102
+ error: 'No active patterns found for this project. Patterns are established over time as agents save learnings.',
103
+ };
104
+ }
105
+ // Get diff
106
+ let diff = getGitDiff(cwd, scope);
107
+ // If staged is empty, try unstaged
108
+ if (!diff && scope === 'staged') {
109
+ diff = getGitDiff(cwd, 'unstaged');
110
+ if (!diff) {
111
+ diff = getGitDiff(cwd, 'last-commit');
112
+ if (diff)
113
+ scope = 'last-commit';
114
+ }
115
+ else {
116
+ scope = 'unstaged';
117
+ }
118
+ }
119
+ if (!diff) {
120
+ return {
121
+ repo: detected.name,
122
+ project_name: detected.name,
123
+ diff_stats: { files_changed: 0, insertions: 0, deletions: 0 },
124
+ patterns_checked: patterns,
125
+ diff_summary: '',
126
+ review_prompt: '',
127
+ error: 'No changes detected. Stage changes, make commits, or specify a scope.',
128
+ };
129
+ }
130
+ const stats = getDiffStats(cwd, scope);
131
+ const files = getChangedFiles(cwd, scope);
132
+ const truncatedDiff = truncateDiff(diff);
133
+ // Build the review prompt - the agent IS the AI reviewer
134
+ const patternList = patterns.map((p, i) => `${i + 1}. **${p.name}** — ${p.description}`).join('\n');
135
+ const reviewPrompt = `## Pattern Compliance Review
136
+
137
+ ### Active Patterns (${patterns.length}):
138
+ ${patternList}
139
+
140
+ ### Changes (${scope}, ${stats.files_changed} files, +${stats.insertions}/-${stats.deletions}):
141
+ Files: ${files.join(', ')}
142
+
143
+ ### Diff:
144
+ \`\`\`diff
145
+ ${truncatedDiff}
146
+ \`\`\`
147
+
148
+ ### Your task:
149
+ Review the diff above against each active pattern. For each pattern:
150
+ - **PASS** if the changes comply or the pattern is not relevant to these changes
151
+ - **VIOLATION** if the changes contradict the pattern - cite the specific line(s)
152
+ - **WARNING** if the changes are in a gray area
153
+
154
+ End with a summary: total violations, total warnings, and whether this is safe to merge.`;
155
+ return {
156
+ repo: detected.name,
157
+ project_name: detected.name,
158
+ diff_stats: stats,
159
+ patterns_checked: patterns,
160
+ diff_summary: `${stats.files_changed} files changed (+${stats.insertions}/-${stats.deletions}): ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5} more` : ''}`,
161
+ review_prompt: reviewPrompt,
162
+ };
163
+ }
@@ -0,0 +1,15 @@
1
+ export declare const reviewCodeSchema: {
2
+ name: "review_code";
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ scope: {
8
+ type: "string";
9
+ enum: string[];
10
+ description: string;
11
+ };
12
+ };
13
+ };
14
+ };
15
+ export declare function handleReviewCode(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { validateAgainstPatterns } from '../lib/pattern-validator.js';
2
+ import * as session from '../lib/session-state.js';
3
+ export const reviewCodeSchema = {
4
+ name: 'review_code',
5
+ description: `Review current code changes against the team's active patterns. Checks your git diff for pattern violations before you commit or open a PR.
6
+
7
+ Use when:
8
+ - Before committing: catch violations early
9
+ - Before opening a PR: ensure compliance with team conventions
10
+ - After making changes: self-review against established patterns
11
+ - When asked to review code quality
12
+
13
+ Scopes:
14
+ - "staged" (default): checks staged changes (git diff --cached)
15
+ - "unstaged": checks working directory changes
16
+ - "last-commit": checks the most recent commit
17
+ - "branch": checks all changes since diverging from main
18
+
19
+ Returns the diff + active patterns formatted for review. You (the agent) are the reviewer - analyze each pattern against the changes and report violations.`,
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ scope: {
24
+ type: 'string',
25
+ enum: ['staged', 'unstaged', 'last-commit', 'branch'],
26
+ description: 'What to review: staged (default), unstaged, last-commit, or branch (all commits since main)',
27
+ },
28
+ },
29
+ },
30
+ };
31
+ export async function handleReviewCode(args) {
32
+ const scope = args.scope || 'staged';
33
+ const report = await validateAgainstPatterns(process.cwd(), scope);
34
+ session.recordToolCall();
35
+ if (report.error) {
36
+ return report.error;
37
+ }
38
+ // Header
39
+ let output = `*Pattern review for ${report.project_name}*\n`;
40
+ output += `${report.diff_summary}\n`;
41
+ output += `Checking against ${report.patterns_checked.length} active pattern(s).\n\n`;
42
+ // The review prompt — agent reads this and does the actual review
43
+ output += report.review_prompt;
44
+ return output;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splicr/mcp-server",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Splicr MCP server — route what you read to what you're building",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",