@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 +7 -2
- package/dist/index.js +9 -0
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/pattern-validator.d.ts +21 -0
- package/dist/lib/pattern-validator.js +163 -0
- package/dist/tools/regenerate-brief.d.ts +9 -0
- package/dist/tools/regenerate-brief.js +60 -0
- package/dist/tools/review-code.d.ts +15 -0
- package/dist/tools/review-code.js +45 -0
- package/package.json +1 -1
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 };
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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,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
|
+
}
|