@splicr/mcp-server 0.11.2 → 0.13.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/cli.js CHANGED
@@ -433,6 +433,11 @@ async function runHook() {
433
433
  }
434
434
  }
435
435
  const [{ results }, patternData] = await Promise.all([contextPromise, patternsPromise]);
436
+ // Build brief section (first message only)
437
+ let briefSection = '';
438
+ if (isFirstMessage && patternData.brief) {
439
+ briefSection = `${patternData.brief}\n\n---\n\n`;
440
+ }
436
441
  // Build patterns section (first message only, deterministic enforcement)
437
442
  let patternsSection = '';
438
443
  if (isFirstMessage && patternData.patterns && patternData.patterns.length > 0) {
@@ -441,7 +446,7 @@ async function runHook() {
441
446
  // Mark patterns as injected for this session
442
447
  saveSessionMeta(sessionId, { patterns_injected: true });
443
448
  }
444
- if ((!results || results.length === 0) && !patternsSection) {
449
+ if ((!results || results.length === 0) && !patternsSection && !briefSection) {
445
450
  process.exit(0);
446
451
  return;
447
452
  }
@@ -466,7 +471,7 @@ async function runHook() {
466
471
  }).join('\n\n');
467
472
  contextSection = `SPLICR CONTEXT — The user's saved research matched this task. Use these findings to inform your response:\n\n${contextLines}\n\nACTION: Review above before answering. Call get_full_content(id) for complete articles. Fall back to web search only if these don't cover the question.`;
468
473
  }
469
- const context = patternsSection + contextSection;
474
+ const context = briefSection + patternsSection + contextSection;
470
475
  if (!context.trim()) {
471
476
  process.exit(0);
472
477
  return;
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ 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';
19
+ import { regenerateBriefSchema, handleRegenerateBrief } from './tools/regenerate-brief.js';
18
20
  import { completeSession } from './lib/api-client.js';
19
21
  // Prevent unhandled errors from crashing the MCP server
20
22
  process.on('uncaughtException', (err) => {
@@ -44,6 +46,9 @@ CONTRACTS (you must follow these):
44
46
  3. Only fall back to web search or training data if Splicr returns no results or results don't cover the question.
45
47
  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
48
 
49
+ QUALITY GATE:
50
+ 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.
51
+
47
52
  FAILURE MODES:
48
53
  - Splicr returns no results → proceed with web search or training data
49
54
  - Splicr API is unreachable → proceed without it, don't block the session
@@ -65,6 +70,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
65
70
  exploreKnowledgeSchema,
66
71
  getDecisionsSchema,
67
72
  getTeamStatusSchema,
73
+ reviewCodeSchema,
74
+ regenerateBriefSchema,
68
75
  ],
69
76
  }));
70
77
  // Handle tool calls with per-tool timeout
@@ -85,6 +92,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
85
92
  explore_knowledge: handleExploreKnowledge,
86
93
  get_decisions: handleGetDecisions,
87
94
  get_team_status: handleGetTeamStatus,
95
+ review_code: handleReviewCode,
96
+ regenerate_brief: handleRegenerateBrief,
88
97
  }[name];
89
98
  if (!handler) {
90
99
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
@@ -13,6 +13,7 @@ export declare function getProjectContext(params: {
13
13
  results: any[];
14
14
  patterns?: any[];
15
15
  project_name?: string;
16
+ brief?: string;
16
17
  }>;
17
18
  export declare function getRecentInsights(params: {
18
19
  days?: number;
@@ -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,9 @@
1
+ export declare const regenerateBriefSchema: {
2
+ name: "regenerate_brief";
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {};
7
+ };
8
+ };
9
+ export declare function handleRegenerateBrief(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,60 @@
1
+ import { detectProject } from '../lib/project-detector.js';
2
+ import { gatherProjectProfile } from '../lib/profile-gatherer.js';
3
+ import * as session from '../lib/session-state.js';
4
+ import { loadAuth } from '../auth.js';
5
+ import { getSessionId } from '../lib/session-state.js';
6
+ const API_URL = process.env.SPLICR_API_URL || 'https://api-production-d889.up.railway.app';
7
+ export const regenerateBriefSchema = {
8
+ name: 'regenerate_brief',
9
+ description: `Regenerate the project onboarding brief. The brief is an auto-generated summary that gives agents instant context about the codebase (architecture, conventions, how-tos, gotchas).
10
+
11
+ Use when:
12
+ - The brief feels outdated or incomplete
13
+ - After significant project changes (new framework, major refactor)
14
+ - When you want to refresh the project context with latest patterns and learnings
15
+ - On first use if no brief exists yet
16
+
17
+ The brief is generated from: project patterns, accumulated learnings, local codebase analysis (directory structure, recent commits, tech stack).`,
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {},
21
+ },
22
+ };
23
+ export async function handleRegenerateBrief(args) {
24
+ const cwd = process.cwd();
25
+ // Detect project
26
+ const detected = await detectProject(cwd).catch(() => null);
27
+ if (!detected) {
28
+ return 'Could not detect project. Register this project with Splicr first.';
29
+ }
30
+ // Gather local profile data for richer brief
31
+ const profileData = gatherProjectProfile(cwd);
32
+ // Call API to regenerate
33
+ const auth = await loadAuth();
34
+ const res = await fetch(`${API_URL}/mcp/regenerate-brief`, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Authorization': `Bearer ${auth.accessToken}`,
38
+ 'Content-Type': 'application/json',
39
+ 'X-Splicr-Session-Id': getSessionId(),
40
+ },
41
+ body: JSON.stringify({
42
+ project_name: detected.name,
43
+ profile_data: {
44
+ recent_commits: profileData.recent_commits,
45
+ directory_structure: profileData.directory_structure,
46
+ },
47
+ }),
48
+ signal: AbortSignal.timeout(30000),
49
+ });
50
+ session.recordToolCall();
51
+ if (!res.ok) {
52
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
53
+ return `Brief regeneration failed: ${err.error || res.status}`;
54
+ }
55
+ const data = await res.json();
56
+ if (data.data?.updated) {
57
+ return `*Brief regenerated for ${detected.name}:*\n\n${data.data.brief}`;
58
+ }
59
+ return data.data?.error || 'Brief regeneration produced no result.';
60
+ }
@@ -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.13.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",