@splicr/mcp-server 0.12.0 → 0.13.1
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 +3 -0
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/profile-gatherer.d.ts +3 -0
- package/dist/lib/profile-gatherer.js +106 -1
- package/dist/tools/regenerate-brief.d.ts +9 -0
- package/dist/tools/regenerate-brief.js +60 -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
|
@@ -16,6 +16,7 @@ import { exploreKnowledgeSchema, handleExploreKnowledge } from './tools/explore-
|
|
|
16
16
|
import { getDecisionsSchema, handleGetDecisions } from './tools/get-decisions.js';
|
|
17
17
|
import { getTeamStatusSchema, handleGetTeamStatus } from './tools/get-team-status.js';
|
|
18
18
|
import { reviewCodeSchema, handleReviewCode } from './tools/review-code.js';
|
|
19
|
+
import { regenerateBriefSchema, handleRegenerateBrief } from './tools/regenerate-brief.js';
|
|
19
20
|
import { completeSession } from './lib/api-client.js';
|
|
20
21
|
// Prevent unhandled errors from crashing the MCP server
|
|
21
22
|
process.on('uncaughtException', (err) => {
|
|
@@ -70,6 +71,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
70
71
|
getDecisionsSchema,
|
|
71
72
|
getTeamStatusSchema,
|
|
72
73
|
reviewCodeSchema,
|
|
74
|
+
regenerateBriefSchema,
|
|
73
75
|
],
|
|
74
76
|
}));
|
|
75
77
|
// Handle tool calls with per-tool timeout
|
|
@@ -91,6 +93,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
91
93
|
get_decisions: handleGetDecisions,
|
|
92
94
|
get_team_status: handleGetTeamStatus,
|
|
93
95
|
review_code: handleReviewCode,
|
|
96
|
+
regenerate_brief: handleRegenerateBrief,
|
|
94
97
|
}[name];
|
|
95
98
|
if (!handler) {
|
|
96
99
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface ProjectProfileData {
|
|
|
12
12
|
recent_commits: string[];
|
|
13
13
|
tech_stack: string[];
|
|
14
14
|
git_remote_url: string | null;
|
|
15
|
+
entry_points: string | null;
|
|
16
|
+
recent_prs: string | null;
|
|
17
|
+
claude_md_rules: string[];
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Gather rich project data from the local filesystem for AI profile generation.
|
|
@@ -10,14 +10,18 @@ const GIT_TIMEOUT = 2000;
|
|
|
10
10
|
* Every operation is individually try/caught — one failure never blocks the rest.
|
|
11
11
|
*/
|
|
12
12
|
export function gatherProjectProfile(cwd) {
|
|
13
|
+
const claudeMd = readFileSafe(join(cwd, 'CLAUDE.md'), MAX_FILE_CHARS);
|
|
13
14
|
return {
|
|
14
|
-
claude_md:
|
|
15
|
+
claude_md: claudeMd,
|
|
15
16
|
readme: readFileSafe(join(cwd, 'README.md'), MAX_FILE_CHARS),
|
|
16
17
|
package_json_subset: readPackageJson(cwd),
|
|
17
18
|
directory_structure: buildDirectoryTree(cwd),
|
|
18
19
|
recent_commits: getRecentCommits(cwd),
|
|
19
20
|
tech_stack: detectTechStack(cwd),
|
|
20
21
|
git_remote_url: getGitRemoteUrl(cwd),
|
|
22
|
+
entry_points: scanEntryPoints(cwd),
|
|
23
|
+
recent_prs: getRecentPRs(cwd),
|
|
24
|
+
claude_md_rules: extractClaudeMdRules(claudeMd),
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
function readFileSafe(path, maxChars) {
|
|
@@ -114,6 +118,107 @@ function buildDirectoryTree(cwd) {
|
|
|
114
118
|
}
|
|
115
119
|
return lines.join('\n') || '(empty)';
|
|
116
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Scan entry point files for architecture context.
|
|
123
|
+
* Reads the first 80 lines of key files (index.ts, app.ts, routes/, etc.)
|
|
124
|
+
*/
|
|
125
|
+
function scanEntryPoints(cwd) {
|
|
126
|
+
const candidates = [
|
|
127
|
+
'src/index.ts', 'src/index.js', 'src/app.ts', 'src/app.js',
|
|
128
|
+
'src/main.ts', 'src/main.js', 'app.ts', 'index.ts',
|
|
129
|
+
'src/server.ts', 'src/server.js',
|
|
130
|
+
'pages/_app.tsx', 'app/layout.tsx', 'app/page.tsx',
|
|
131
|
+
'src/App.tsx', 'src/App.jsx',
|
|
132
|
+
];
|
|
133
|
+
const found = [];
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const fullPath = join(cwd, candidate);
|
|
136
|
+
if (existsSync(fullPath)) {
|
|
137
|
+
try {
|
|
138
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
139
|
+
const lines = content.split('\n').slice(0, 80);
|
|
140
|
+
// Extract imports and exports for architecture understanding
|
|
141
|
+
const importExportLines = lines.filter(l => l.trim().startsWith('import ') || l.trim().startsWith('export ') ||
|
|
142
|
+
l.trim().startsWith('app.') || l.trim().startsWith('router.') ||
|
|
143
|
+
l.trim().startsWith('const app') || l.trim().startsWith('const server'));
|
|
144
|
+
if (importExportLines.length > 0) {
|
|
145
|
+
found.push(`--- ${candidate} ---\n${importExportLines.join('\n')}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { /* skip */ }
|
|
149
|
+
}
|
|
150
|
+
if (found.length >= 3)
|
|
151
|
+
break; // Cap at 3 entry points
|
|
152
|
+
}
|
|
153
|
+
// Also scan for route definitions
|
|
154
|
+
const routeDirs = ['src/routes', 'src/pages', 'src/api', 'app/api', 'routes'];
|
|
155
|
+
for (const routeDir of routeDirs) {
|
|
156
|
+
const fullDir = join(cwd, routeDir);
|
|
157
|
+
if (existsSync(fullDir)) {
|
|
158
|
+
try {
|
|
159
|
+
const files = readdirSync(fullDir).filter(f => !f.startsWith('.')).slice(0, 15);
|
|
160
|
+
found.push(`--- ${routeDir}/ ---\n${files.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
catch { /* skip */ }
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return found.length > 0 ? found.join('\n\n').substring(0, 2000) : null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get recent merged PR titles + descriptions via gh CLI.
|
|
170
|
+
* Rich "why" context that commit messages lack.
|
|
171
|
+
*/
|
|
172
|
+
function getRecentPRs(cwd) {
|
|
173
|
+
try {
|
|
174
|
+
// Check if gh is available
|
|
175
|
+
execSync('gh auth status', { encoding: 'utf-8', stdio: 'pipe', timeout: 3000 });
|
|
176
|
+
const result = execSync('gh pr list --state merged --limit 10 --json title,body,mergedAt --jq ".[] | \\"- \\(.title)\\" + if .body != \\"\\" then \\"\\n \\(.body | split(\\"\\\\n\\") | first)\\" else \\"\\" end"', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
|
|
177
|
+
return result || null;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// gh not available or not in a GitHub repo - try simpler approach
|
|
181
|
+
try {
|
|
182
|
+
const result = execSync('gh pr list --state merged --limit 10 --json title --jq ".[].title"', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
|
|
183
|
+
return result ? result.split('\n').map(t => `- ${t}`).join('\n') : null;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Extract actionable rules from CLAUDE.md content.
|
|
192
|
+
* Looks for bullet points, numbered lists, and imperative statements.
|
|
193
|
+
*/
|
|
194
|
+
function extractClaudeMdRules(claudeMd) {
|
|
195
|
+
if (!claudeMd)
|
|
196
|
+
return [];
|
|
197
|
+
const rules = [];
|
|
198
|
+
const lines = claudeMd.split('\n');
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const trimmed = line.trim();
|
|
201
|
+
// Skip headers, empty lines, code blocks, tables
|
|
202
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```') ||
|
|
203
|
+
trimmed.startsWith('|') || trimmed.startsWith('---'))
|
|
204
|
+
continue;
|
|
205
|
+
// Look for rule-like patterns
|
|
206
|
+
const isRule =
|
|
207
|
+
// Bullet/numbered items with imperative verbs
|
|
208
|
+
(/^[-*]\s+(Always|Never|Don't|Do not|Must|Prefer|Avoid|Use|Keep|Follow|Ensure)/i.test(trimmed)) ||
|
|
209
|
+
(/^\d+\.\s+(Always|Never|Don't|Do not|Must|Prefer|Avoid|Use|Keep|Follow|Ensure)/i.test(trimmed)) ||
|
|
210
|
+
// Lines containing strong conventions
|
|
211
|
+
(/\b(must|always|never|required|forbidden|mandatory)\b/i.test(trimmed) && trimmed.length < 200);
|
|
212
|
+
if (isRule) {
|
|
213
|
+
// Clean up the rule text
|
|
214
|
+
const cleaned = trimmed.replace(/^[-*\d.]+\s*/, '').trim();
|
|
215
|
+
if (cleaned.length > 15 && cleaned.length < 300) {
|
|
216
|
+
rules.push(cleaned);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return rules.slice(0, 20); // Cap at 20 rules
|
|
221
|
+
}
|
|
117
222
|
// Duplicated from signal-gatherer.ts to avoid circular imports
|
|
118
223
|
const KNOWN_TECH = [
|
|
119
224
|
'react', 'next', 'express', 'fastify', 'vue', 'angular', 'svelte',
|
|
@@ -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
|
+
}
|