@splicr/mcp-server 0.13.1 → 0.14.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 +247 -1
- package/dist/lib/api-client.d.ts +19 -0
- package/dist/lib/api-client.js +10 -0
- package/dist/lib/file-parser.d.ts +11 -0
- package/dist/lib/file-parser.js +329 -0
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
11
|
import { execSync } from 'child_process';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
|
-
import { join } from 'path';
|
|
13
|
+
import { join, basename } from 'path';
|
|
14
14
|
const command = process.argv[2];
|
|
15
15
|
const subCommand = process.argv[3];
|
|
16
16
|
// Parse --join flag from any position
|
|
@@ -195,6 +195,12 @@ async function main() {
|
|
|
195
195
|
case 'stop':
|
|
196
196
|
await runStop();
|
|
197
197
|
break;
|
|
198
|
+
case 'index':
|
|
199
|
+
await runIndex();
|
|
200
|
+
break;
|
|
201
|
+
case 'webhook':
|
|
202
|
+
await webhookCommand();
|
|
203
|
+
break;
|
|
198
204
|
case 'dashboard':
|
|
199
205
|
case 'open': {
|
|
200
206
|
const url = 'https://splicr.dev/dashboard';
|
|
@@ -1142,6 +1148,243 @@ function printManualConfig() {
|
|
|
1142
1148
|
console.error(' }');
|
|
1143
1149
|
console.error(' }\n');
|
|
1144
1150
|
}
|
|
1151
|
+
// ─── splicr webhook ───
|
|
1152
|
+
async function webhookCommand() {
|
|
1153
|
+
const { hasAuth } = await import('./auth.js');
|
|
1154
|
+
if (!hasAuth()) {
|
|
1155
|
+
console.error('Not authenticated. Run: splicr setup');
|
|
1156
|
+
process.exit(1);
|
|
1157
|
+
}
|
|
1158
|
+
if (subCommand !== 'setup') {
|
|
1159
|
+
console.error(`
|
|
1160
|
+
Splicr Webhooks
|
|
1161
|
+
|
|
1162
|
+
Commands:
|
|
1163
|
+
webhook setup Generate webhook secret + show GitHub config instructions
|
|
1164
|
+
|
|
1165
|
+
Auto-extracts patterns from PR review comments.
|
|
1166
|
+
When a reviewer says "use X, not Y", Splicr registers it as a team pattern.
|
|
1167
|
+
`);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const { listTeams, setupWebhook } = await import('./lib/api-client.js');
|
|
1171
|
+
// Get user's teams
|
|
1172
|
+
let teams;
|
|
1173
|
+
try {
|
|
1174
|
+
teams = await listTeams();
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
console.error('Failed to load teams.');
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
if (!teams || teams.length === 0) {
|
|
1181
|
+
console.error('You need a team first. Create one:');
|
|
1182
|
+
console.error(' splicr team create "My Team"');
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
const team = teams[0];
|
|
1186
|
+
console.error(`Setting up GitHub webhook for team "${team.name}"...\n`);
|
|
1187
|
+
try {
|
|
1188
|
+
const result = await setupWebhook(team.id);
|
|
1189
|
+
console.error('Webhook secret generated. Configure GitHub:\n');
|
|
1190
|
+
console.error(` 1. Go to your repo -> Settings -> Webhooks -> Add webhook`);
|
|
1191
|
+
console.error(` 2. Payload URL: ${result.webhook_url}`);
|
|
1192
|
+
console.error(` 3. Content type: application/json`);
|
|
1193
|
+
console.error(` 4. Secret: ${result.webhook_secret}`);
|
|
1194
|
+
console.error(` 5. Events: Select "Pull request review comments"`);
|
|
1195
|
+
console.error(` 6. Click "Add webhook"\n`);
|
|
1196
|
+
console.error('Once configured, Splicr auto-extracts patterns from PR reviews.');
|
|
1197
|
+
console.error('Every agent on the team will follow these patterns.\n');
|
|
1198
|
+
}
|
|
1199
|
+
catch (err) {
|
|
1200
|
+
console.error(`Failed: ${err.message}`);
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// ─── splicr index ───
|
|
1205
|
+
async function runIndex() {
|
|
1206
|
+
const target = process.argv[3];
|
|
1207
|
+
if (!target) {
|
|
1208
|
+
console.error('Usage: splicr index <path-or-url>');
|
|
1209
|
+
console.error(' splicr index ./docs/ Index all supported files in directory');
|
|
1210
|
+
console.error(' splicr index ./api.yaml Index a single file');
|
|
1211
|
+
console.error(' splicr index https://example.com Index a URL');
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
const { hasAuth } = await import('./auth.js');
|
|
1215
|
+
if (!hasAuth()) {
|
|
1216
|
+
console.error('Not authenticated. Run: splicr setup');
|
|
1217
|
+
process.exit(1);
|
|
1218
|
+
}
|
|
1219
|
+
// Detect project for routing
|
|
1220
|
+
const { detectProject } = await import('./lib/project-detector.js');
|
|
1221
|
+
const project = await detectProject(process.cwd()).catch(() => null);
|
|
1222
|
+
const projectName = project?.name;
|
|
1223
|
+
if (projectName) {
|
|
1224
|
+
console.error(`Project: ${projectName}`);
|
|
1225
|
+
}
|
|
1226
|
+
// URL?
|
|
1227
|
+
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
1228
|
+
await indexUrl(target, projectName);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// File or directory
|
|
1232
|
+
const { resolve } = await import('path');
|
|
1233
|
+
const { statSync } = await import('fs');
|
|
1234
|
+
const resolved = resolve(process.cwd(), target);
|
|
1235
|
+
let stat;
|
|
1236
|
+
try {
|
|
1237
|
+
stat = statSync(resolved);
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
console.error(`Not found: ${resolved}`);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
if (stat.isDirectory()) {
|
|
1244
|
+
await indexDirectory(resolved, projectName);
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
const result = await indexSingleFile(resolved, projectName);
|
|
1248
|
+
if (result) {
|
|
1249
|
+
console.error(`\nDone: 1 ${result}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function findGitRoot(startPath) {
|
|
1254
|
+
try {
|
|
1255
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
1256
|
+
cwd: startPath,
|
|
1257
|
+
encoding: 'utf-8',
|
|
1258
|
+
timeout: 2000,
|
|
1259
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1260
|
+
}).trim();
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async function indexSingleFile(filePath, projectName) {
|
|
1267
|
+
const { parseFile } = await import('./lib/file-parser.js');
|
|
1268
|
+
const { indexFile } = await import('./lib/api-client.js');
|
|
1269
|
+
const { normalizePath } = await import('./lib/project-detector.js');
|
|
1270
|
+
const { relative, dirname } = await import('path');
|
|
1271
|
+
let parsed;
|
|
1272
|
+
try {
|
|
1273
|
+
parsed = parseFile(filePath);
|
|
1274
|
+
}
|
|
1275
|
+
catch (err) {
|
|
1276
|
+
const short = basename(filePath);
|
|
1277
|
+
console.error(` x ${short} - ${err.message}`);
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
// Build source_url as file:// relative to git root
|
|
1281
|
+
const gitRoot = findGitRoot(dirname(filePath)) || process.cwd();
|
|
1282
|
+
const relPath = normalizePath(relative(gitRoot, filePath));
|
|
1283
|
+
const sourceUrl = `file://${relPath}`;
|
|
1284
|
+
const shortPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
|
|
1285
|
+
try {
|
|
1286
|
+
const result = await indexFile({
|
|
1287
|
+
content: parsed.content,
|
|
1288
|
+
title: parsed.title,
|
|
1289
|
+
source_url: sourceUrl,
|
|
1290
|
+
file_type: parsed.file_type,
|
|
1291
|
+
project_name: projectName,
|
|
1292
|
+
tags: [...parsed.tags, 'indexed'],
|
|
1293
|
+
content_category: parsed.content_category,
|
|
1294
|
+
metadata: { word_count: parsed.word_count, file_type: parsed.file_type },
|
|
1295
|
+
});
|
|
1296
|
+
const status = result.status === 'updated' ? 'updated' : 'created';
|
|
1297
|
+
console.error(` ${status === 'updated' ? '~' : '+'} ${shortPath}`);
|
|
1298
|
+
return status;
|
|
1299
|
+
}
|
|
1300
|
+
catch (err) {
|
|
1301
|
+
console.error(` x ${shortPath} - ${err.message}`);
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function indexDirectory(dirPath, projectName) {
|
|
1306
|
+
const { readdirSync, statSync: fsStat } = await import('fs');
|
|
1307
|
+
const { join: pathJoin } = await import('path');
|
|
1308
|
+
const { isSupported, getExtension } = await import('./lib/file-parser.js');
|
|
1309
|
+
const SKIP_DIRS = new Set([
|
|
1310
|
+
'node_modules', '.git', 'dist', 'build', 'coverage',
|
|
1311
|
+
'.next', '__pycache__', '.turbo', '.cache', '.expo',
|
|
1312
|
+
'vendor', '.venv', 'venv', 'target', '.output',
|
|
1313
|
+
]);
|
|
1314
|
+
const MAX_FILES = 50;
|
|
1315
|
+
const files = [];
|
|
1316
|
+
function walk(dir) {
|
|
1317
|
+
if (files.length >= MAX_FILES)
|
|
1318
|
+
return;
|
|
1319
|
+
try {
|
|
1320
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1321
|
+
for (const entry of entries) {
|
|
1322
|
+
if (files.length >= MAX_FILES)
|
|
1323
|
+
return;
|
|
1324
|
+
if (entry.name.startsWith('.'))
|
|
1325
|
+
continue;
|
|
1326
|
+
const full = pathJoin(dir, entry.name);
|
|
1327
|
+
if (entry.isDirectory()) {
|
|
1328
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
1329
|
+
walk(full);
|
|
1330
|
+
}
|
|
1331
|
+
else if (isSupported(getExtension(entry.name))) {
|
|
1332
|
+
files.push(full);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
catch { /* skip unreadable dirs */ }
|
|
1337
|
+
}
|
|
1338
|
+
walk(dirPath);
|
|
1339
|
+
if (files.length === 0) {
|
|
1340
|
+
console.error('No supported files found.');
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
console.error(`Indexing ${files.length} file(s)${files.length >= MAX_FILES ? ` (capped at ${MAX_FILES})` : ''}...\n`);
|
|
1344
|
+
let created = 0;
|
|
1345
|
+
let updated = 0;
|
|
1346
|
+
let failed = 0;
|
|
1347
|
+
// Process with concurrency limit of 5
|
|
1348
|
+
const CONCURRENCY = 5;
|
|
1349
|
+
for (let i = 0; i < files.length; i += CONCURRENCY) {
|
|
1350
|
+
const batch = files.slice(i, i + CONCURRENCY);
|
|
1351
|
+
const results = await Promise.allSettled(batch.map(f => indexSingleFile(f, projectName)));
|
|
1352
|
+
for (const r of results) {
|
|
1353
|
+
if (r.status === 'fulfilled' && r.value) {
|
|
1354
|
+
if (r.value === 'updated')
|
|
1355
|
+
updated++;
|
|
1356
|
+
else
|
|
1357
|
+
created++;
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
failed++;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
console.error(`\nDone: ${created} indexed, ${updated} updated, ${failed} failed`);
|
|
1365
|
+
}
|
|
1366
|
+
async function indexUrl(url, projectName) {
|
|
1367
|
+
const { saveFromAgent } = await import('./lib/api-client.js');
|
|
1368
|
+
console.error(`Indexing URL: ${url}`);
|
|
1369
|
+
try {
|
|
1370
|
+
const result = await saveFromAgent({
|
|
1371
|
+
content: url,
|
|
1372
|
+
title: url,
|
|
1373
|
+
project_name: projectName,
|
|
1374
|
+
tags: ['indexed', 'url'],
|
|
1375
|
+
});
|
|
1376
|
+
if (result.duplicate) {
|
|
1377
|
+
console.error(' ~ Already indexed');
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
console.error(` + Saved`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
catch (err) {
|
|
1384
|
+
console.error(` x Failed: ${err.message}`);
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1145
1388
|
function printHelp() {
|
|
1146
1389
|
console.error(`
|
|
1147
1390
|
Splicr — route what you read to what you're building
|
|
@@ -1157,6 +1400,9 @@ function printHelp() {
|
|
|
1157
1400
|
team list List your teams
|
|
1158
1401
|
team invite Show invite link for your team
|
|
1159
1402
|
team join <code> Join a team by invite code
|
|
1403
|
+
index <path> Index local files into your knowledge base
|
|
1404
|
+
index <dir> Index all supported files in a directory
|
|
1405
|
+
webhook setup Set up GitHub PR webhook (auto-extract patterns from reviews)
|
|
1160
1406
|
dashboard Open knowledge dashboard in browser
|
|
1161
1407
|
uninstall Remove Splicr from all coding agents
|
|
1162
1408
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export declare function saveFromAgent(params: {
|
|
|
36
36
|
project_name?: string;
|
|
37
37
|
duplicate: boolean;
|
|
38
38
|
}>;
|
|
39
|
+
export declare function indexFile(params: {
|
|
40
|
+
content: string;
|
|
41
|
+
title: string;
|
|
42
|
+
source_url: string;
|
|
43
|
+
file_type?: string;
|
|
44
|
+
project_name?: string;
|
|
45
|
+
tags?: string[];
|
|
46
|
+
content_category?: string;
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
status: 'created' | 'updated';
|
|
52
|
+
}>;
|
|
53
|
+
export declare function listTeams(): Promise<any[]>;
|
|
54
|
+
export declare function setupWebhook(teamId: string): Promise<{
|
|
55
|
+
webhook_secret: string;
|
|
56
|
+
webhook_url: string;
|
|
57
|
+
}>;
|
|
39
58
|
export declare function resolveProject(params: {
|
|
40
59
|
local_path?: string;
|
|
41
60
|
git_remote_url?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -58,6 +58,16 @@ export async function getRecentInsights(params) {
|
|
|
58
58
|
export async function saveFromAgent(params) {
|
|
59
59
|
return await apiRequest('POST', '/mcp/save', params);
|
|
60
60
|
}
|
|
61
|
+
export async function indexFile(params) {
|
|
62
|
+
return await apiRequest('POST', '/mcp/index', params);
|
|
63
|
+
}
|
|
64
|
+
export async function listTeams() {
|
|
65
|
+
const data = await apiRequest('GET', '/teams');
|
|
66
|
+
return data ?? [];
|
|
67
|
+
}
|
|
68
|
+
export async function setupWebhook(teamId) {
|
|
69
|
+
return await apiRequest('POST', `/teams/${teamId}/webhook`, {});
|
|
70
|
+
}
|
|
61
71
|
export async function resolveProject(params) {
|
|
62
72
|
return await apiRequest('POST', '/mcp/resolve-project', params);
|
|
63
73
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ParsedFile {
|
|
2
|
+
title: string;
|
|
3
|
+
content: string;
|
|
4
|
+
file_type: string;
|
|
5
|
+
content_category: string;
|
|
6
|
+
tags: string[];
|
|
7
|
+
word_count: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function getExtension(filename: string): string;
|
|
10
|
+
export declare function isSupported(ext: string): boolean;
|
|
11
|
+
export declare function parseFile(filePath: string): ParsedFile;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'fs';
|
|
2
|
+
import { basename, extname } from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
const MAX_RAW_SIZE = 1024 * 1024; // 1MB
|
|
5
|
+
const MAX_CONTENT_CHARS = 50_000;
|
|
6
|
+
const MAX_OPENAPI_CHARS = 20_000;
|
|
7
|
+
const MAX_CODE_CHARS = 10_000;
|
|
8
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
9
|
+
'.md', '.txt', '.rst', '.adoc',
|
|
10
|
+
'.yaml', '.yml', '.json',
|
|
11
|
+
'.ts', '.tsx', '.js', '.jsx',
|
|
12
|
+
'.py', '.go', '.rs', '.java',
|
|
13
|
+
'.css', '.scss',
|
|
14
|
+
]);
|
|
15
|
+
export function getExtension(filename) {
|
|
16
|
+
return extname(filename).toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
export function isSupported(ext) {
|
|
19
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
20
|
+
}
|
|
21
|
+
export function parseFile(filePath) {
|
|
22
|
+
const stat = statSync(filePath);
|
|
23
|
+
if (stat.size > MAX_RAW_SIZE) {
|
|
24
|
+
throw new Error(`File too large (${(stat.size / 1024).toFixed(0)}KB > 1MB limit)`);
|
|
25
|
+
}
|
|
26
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
27
|
+
const filename = basename(filePath);
|
|
28
|
+
const ext = getExtension(filename);
|
|
29
|
+
switch (ext) {
|
|
30
|
+
case '.md':
|
|
31
|
+
case '.rst':
|
|
32
|
+
case '.adoc':
|
|
33
|
+
return parseMarkdown(raw, filename);
|
|
34
|
+
case '.txt':
|
|
35
|
+
return parseText(raw, filename);
|
|
36
|
+
case '.yaml':
|
|
37
|
+
case '.yml':
|
|
38
|
+
return parseYamlOrOpenApi(raw, filename);
|
|
39
|
+
case '.json':
|
|
40
|
+
return parseJson(raw, filename);
|
|
41
|
+
case '.ts':
|
|
42
|
+
case '.tsx':
|
|
43
|
+
case '.js':
|
|
44
|
+
case '.jsx':
|
|
45
|
+
return parseCodeSurface(raw, filename, 'typescript');
|
|
46
|
+
case '.py':
|
|
47
|
+
return parseCodeSurface(raw, filename, 'python');
|
|
48
|
+
case '.go':
|
|
49
|
+
return parseCodeSurface(raw, filename, 'go');
|
|
50
|
+
case '.rs':
|
|
51
|
+
return parseCodeSurface(raw, filename, 'rust');
|
|
52
|
+
case '.java':
|
|
53
|
+
return parseCodeSurface(raw, filename, 'java');
|
|
54
|
+
case '.css':
|
|
55
|
+
case '.scss':
|
|
56
|
+
return parseText(raw, filename);
|
|
57
|
+
default:
|
|
58
|
+
return parseText(raw, filename);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// --- Parsers ---
|
|
62
|
+
function parseMarkdown(raw, filename) {
|
|
63
|
+
const tags = ['markdown'];
|
|
64
|
+
// Extract frontmatter tags if present
|
|
65
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
66
|
+
if (fmMatch) {
|
|
67
|
+
const fmContent = fmMatch[1];
|
|
68
|
+
const tagMatch = fmContent.match(/tags:\s*\[([^\]]+)\]/);
|
|
69
|
+
if (tagMatch) {
|
|
70
|
+
tags.push(...tagMatch[1].split(',').map(t => t.trim().replace(/['"]/g, '')));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extract title from first heading
|
|
74
|
+
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
75
|
+
const title = titleMatch ? titleMatch[1].trim() : filename;
|
|
76
|
+
const content = raw.substring(0, MAX_CONTENT_CHARS);
|
|
77
|
+
return {
|
|
78
|
+
title,
|
|
79
|
+
content,
|
|
80
|
+
file_type: 'markdown',
|
|
81
|
+
content_category: 'other',
|
|
82
|
+
tags,
|
|
83
|
+
word_count: content.split(/\s+/).length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function parseText(raw, filename) {
|
|
87
|
+
const content = raw.substring(0, MAX_CONTENT_CHARS);
|
|
88
|
+
return {
|
|
89
|
+
title: filename,
|
|
90
|
+
content,
|
|
91
|
+
file_type: 'text',
|
|
92
|
+
content_category: 'other',
|
|
93
|
+
tags: ['text'],
|
|
94
|
+
word_count: content.split(/\s+/).length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseJson(raw, filename) {
|
|
98
|
+
// For package.json, extract useful summary
|
|
99
|
+
if (filename === 'package.json') {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(raw);
|
|
102
|
+
const lines = [];
|
|
103
|
+
if (pkg.name)
|
|
104
|
+
lines.push(`Package: ${pkg.name}`);
|
|
105
|
+
if (pkg.description)
|
|
106
|
+
lines.push(`Description: ${pkg.description}`);
|
|
107
|
+
if (pkg.scripts)
|
|
108
|
+
lines.push(`\nScripts:\n${Object.entries(pkg.scripts).map(([k, v]) => ` ${k}: ${v}`).join('\n')}`);
|
|
109
|
+
if (pkg.dependencies)
|
|
110
|
+
lines.push(`\nDependencies:\n${Object.keys(pkg.dependencies).map(d => ` - ${d}`).join('\n')}`);
|
|
111
|
+
if (pkg.devDependencies)
|
|
112
|
+
lines.push(`\nDev Dependencies:\n${Object.keys(pkg.devDependencies).map(d => ` - ${d}`).join('\n')}`);
|
|
113
|
+
return {
|
|
114
|
+
title: pkg.name || filename,
|
|
115
|
+
content: lines.join('\n'),
|
|
116
|
+
file_type: 'json',
|
|
117
|
+
content_category: 'tool',
|
|
118
|
+
tags: ['json', 'package'],
|
|
119
|
+
word_count: lines.join('\n').split(/\s+/).length,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch { /* fall through to raw */ }
|
|
123
|
+
}
|
|
124
|
+
const content = raw.substring(0, MAX_CONTENT_CHARS);
|
|
125
|
+
return {
|
|
126
|
+
title: filename,
|
|
127
|
+
content,
|
|
128
|
+
file_type: 'json',
|
|
129
|
+
content_category: 'other',
|
|
130
|
+
tags: ['json'],
|
|
131
|
+
word_count: content.split(/\s+/).length,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseYamlOrOpenApi(raw, filename) {
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = yaml.load(raw);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Invalid YAML - treat as text
|
|
141
|
+
return parseText(raw, filename);
|
|
142
|
+
}
|
|
143
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
144
|
+
return parseText(raw, filename);
|
|
145
|
+
}
|
|
146
|
+
// Detect OpenAPI
|
|
147
|
+
if (parsed.openapi || parsed.swagger) {
|
|
148
|
+
return parseOpenApi(parsed, filename);
|
|
149
|
+
}
|
|
150
|
+
// Regular YAML - return as-is
|
|
151
|
+
const content = raw.substring(0, MAX_CONTENT_CHARS);
|
|
152
|
+
return {
|
|
153
|
+
title: filename,
|
|
154
|
+
content,
|
|
155
|
+
file_type: 'yaml',
|
|
156
|
+
content_category: 'other',
|
|
157
|
+
tags: ['yaml'],
|
|
158
|
+
word_count: content.split(/\s+/).length,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function parseOpenApi(spec, filename) {
|
|
162
|
+
const lines = [];
|
|
163
|
+
// Header
|
|
164
|
+
const title = spec.info?.title || filename;
|
|
165
|
+
const version = spec.info?.version || '';
|
|
166
|
+
lines.push(`OpenAPI: ${title}${version ? ` v${version}` : ''}`);
|
|
167
|
+
if (spec.info?.description) {
|
|
168
|
+
lines.push(`Description: ${spec.info.description.substring(0, 200)}`);
|
|
169
|
+
}
|
|
170
|
+
// Servers
|
|
171
|
+
if (spec.servers?.length) {
|
|
172
|
+
lines.push(`Base URL: ${spec.servers[0].url}`);
|
|
173
|
+
}
|
|
174
|
+
// Auth
|
|
175
|
+
const securitySchemes = spec.components?.securitySchemes;
|
|
176
|
+
if (securitySchemes) {
|
|
177
|
+
const schemes = Object.entries(securitySchemes).map(([name, s]) => {
|
|
178
|
+
if (s.type === 'http')
|
|
179
|
+
return `${name}: ${s.scheme} ${s.type}`;
|
|
180
|
+
if (s.type === 'apiKey')
|
|
181
|
+
return `${name}: API key in ${s.in}`;
|
|
182
|
+
if (s.type === 'oauth2')
|
|
183
|
+
return `${name}: OAuth2`;
|
|
184
|
+
return `${name}: ${s.type}`;
|
|
185
|
+
});
|
|
186
|
+
lines.push(`Auth: ${schemes.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
// Endpoints
|
|
189
|
+
if (spec.paths) {
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('Endpoints:');
|
|
192
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
193
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
194
|
+
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
|
|
195
|
+
const operation = op;
|
|
196
|
+
const summary = operation.summary || operation.operationId || '';
|
|
197
|
+
lines.push(` ${method.toUpperCase()} ${path}${summary ? ` - ${summary}` : ''}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Schemas
|
|
203
|
+
const schemas = spec.components?.schemas || spec.definitions;
|
|
204
|
+
if (schemas) {
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push('Schemas:');
|
|
207
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
208
|
+
const s = schema;
|
|
209
|
+
const props = s.properties ? Object.entries(s.properties).map(([k, v]) => `${k} (${v.type || 'object'})`).join(', ') : '';
|
|
210
|
+
lines.push(` ${name}${props ? `: ${props}` : ''}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const content = lines.join('\n').substring(0, MAX_OPENAPI_CHARS);
|
|
214
|
+
return {
|
|
215
|
+
title: `API: ${title}`,
|
|
216
|
+
content,
|
|
217
|
+
file_type: 'openapi',
|
|
218
|
+
content_category: 'architecture',
|
|
219
|
+
tags: ['openapi', 'api'],
|
|
220
|
+
word_count: content.split(/\s+/).length,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function parseCodeSurface(raw, filename, lang) {
|
|
224
|
+
const lines = raw.split('\n');
|
|
225
|
+
const surface = [];
|
|
226
|
+
surface.push(`// File: ${filename}`);
|
|
227
|
+
surface.push(`// Language: ${lang}`);
|
|
228
|
+
surface.push('');
|
|
229
|
+
let insideBlock = false;
|
|
230
|
+
let braceDepth = 0;
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
|
|
234
|
+
continue;
|
|
235
|
+
// Track if we're capturing a multi-line interface/type body
|
|
236
|
+
if (insideBlock) {
|
|
237
|
+
surface.push(line);
|
|
238
|
+
braceDepth += (line.match(/\{/g) || []).length;
|
|
239
|
+
braceDepth -= (line.match(/\}/g) || []).length;
|
|
240
|
+
if (braceDepth <= 0) {
|
|
241
|
+
insideBlock = false;
|
|
242
|
+
braceDepth = 0;
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (lang === 'typescript') {
|
|
247
|
+
// Imports
|
|
248
|
+
if (trimmed.startsWith('import ')) {
|
|
249
|
+
surface.push(trimmed);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Export statements, interfaces, types, classes, functions, enums
|
|
253
|
+
if (/^export\s/.test(trimmed) || /^(interface|type|class|enum|abstract)\s/.test(trimmed)) {
|
|
254
|
+
surface.push(trimmed);
|
|
255
|
+
// If line opens a block, capture the full body (for interfaces/types)
|
|
256
|
+
if (trimmed.includes('{') && !trimmed.includes('}')) {
|
|
257
|
+
insideBlock = true;
|
|
258
|
+
braceDepth = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (lang === 'python') {
|
|
264
|
+
if (/^(from |import )/.test(trimmed)) {
|
|
265
|
+
surface.push(trimmed);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (/^(def |class |async def )/.test(trimmed)) {
|
|
269
|
+
surface.push(trimmed);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// Decorators
|
|
273
|
+
if (trimmed.startsWith('@')) {
|
|
274
|
+
surface.push(trimmed);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (lang === 'go') {
|
|
279
|
+
if (/^package\s/.test(trimmed)) {
|
|
280
|
+
surface.push(trimmed);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (/^import\s/.test(trimmed) || trimmed === 'import (') {
|
|
284
|
+
surface.push(trimmed);
|
|
285
|
+
if (trimmed.includes('(') && !trimmed.includes(')')) {
|
|
286
|
+
insideBlock = true;
|
|
287
|
+
braceDepth = 1;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (/^(func |type |var |const )/.test(trimmed)) {
|
|
292
|
+
surface.push(trimmed);
|
|
293
|
+
if (trimmed.includes('{') && !trimmed.includes('}')) {
|
|
294
|
+
// For func, just capture the signature, not the body
|
|
295
|
+
if (trimmed.startsWith('func '))
|
|
296
|
+
continue;
|
|
297
|
+
insideBlock = true;
|
|
298
|
+
braceDepth = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (lang === 'rust') {
|
|
304
|
+
if (/^(pub |use |mod |fn |struct |enum |trait |impl |type |const )/.test(trimmed)) {
|
|
305
|
+
surface.push(trimmed);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else if (lang === 'java') {
|
|
310
|
+
if (/^(import |package )/.test(trimmed)) {
|
|
311
|
+
surface.push(trimmed);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (/^(public |private |protected |static |abstract |interface |class |enum |@)/.test(trimmed)) {
|
|
315
|
+
surface.push(trimmed);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const content = surface.join('\n').substring(0, MAX_CODE_CHARS);
|
|
321
|
+
return {
|
|
322
|
+
title: filename,
|
|
323
|
+
content: content || `// File: ${filename}\n// No public surface detected`,
|
|
324
|
+
file_type: lang,
|
|
325
|
+
content_category: 'code_snippet',
|
|
326
|
+
tags: [lang, 'code'],
|
|
327
|
+
word_count: content.split(/\s+/).length,
|
|
328
|
+
};
|
|
329
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@splicr/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
33
|
+
"js-yaml": "^4.1.0",
|
|
33
34
|
"zod": "^3.23.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@types/js-yaml": "^4.0.9",
|
|
36
38
|
"@types/node": "^20.14.0",
|
|
37
39
|
"tsx": "^4.16.0",
|
|
38
40
|
"typescript": "^5.5.0"
|