@splicr/mcp-server 0.13.1 → 0.14.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 +190 -1
- package/dist/lib/api-client.d.ts +14 -0
- package/dist/lib/api-client.js +3 -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,9 @@ async function main() {
|
|
|
195
195
|
case 'stop':
|
|
196
196
|
await runStop();
|
|
197
197
|
break;
|
|
198
|
+
case 'index':
|
|
199
|
+
await runIndex();
|
|
200
|
+
break;
|
|
198
201
|
case 'dashboard':
|
|
199
202
|
case 'open': {
|
|
200
203
|
const url = 'https://splicr.dev/dashboard';
|
|
@@ -1142,6 +1145,190 @@ function printManualConfig() {
|
|
|
1142
1145
|
console.error(' }');
|
|
1143
1146
|
console.error(' }\n');
|
|
1144
1147
|
}
|
|
1148
|
+
// ─── splicr index ───
|
|
1149
|
+
async function runIndex() {
|
|
1150
|
+
const target = process.argv[3];
|
|
1151
|
+
if (!target) {
|
|
1152
|
+
console.error('Usage: splicr index <path-or-url>');
|
|
1153
|
+
console.error(' splicr index ./docs/ Index all supported files in directory');
|
|
1154
|
+
console.error(' splicr index ./api.yaml Index a single file');
|
|
1155
|
+
console.error(' splicr index https://example.com Index a URL');
|
|
1156
|
+
process.exit(1);
|
|
1157
|
+
}
|
|
1158
|
+
const { hasAuth } = await import('./auth.js');
|
|
1159
|
+
if (!hasAuth()) {
|
|
1160
|
+
console.error('Not authenticated. Run: splicr setup');
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
// Detect project for routing
|
|
1164
|
+
const { detectProject } = await import('./lib/project-detector.js');
|
|
1165
|
+
const project = await detectProject(process.cwd()).catch(() => null);
|
|
1166
|
+
const projectName = project?.name;
|
|
1167
|
+
if (projectName) {
|
|
1168
|
+
console.error(`Project: ${projectName}`);
|
|
1169
|
+
}
|
|
1170
|
+
// URL?
|
|
1171
|
+
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
1172
|
+
await indexUrl(target, projectName);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// File or directory
|
|
1176
|
+
const { resolve } = await import('path');
|
|
1177
|
+
const { statSync } = await import('fs');
|
|
1178
|
+
const resolved = resolve(process.cwd(), target);
|
|
1179
|
+
let stat;
|
|
1180
|
+
try {
|
|
1181
|
+
stat = statSync(resolved);
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
console.error(`Not found: ${resolved}`);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
if (stat.isDirectory()) {
|
|
1188
|
+
await indexDirectory(resolved, projectName);
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
const result = await indexSingleFile(resolved, projectName);
|
|
1192
|
+
if (result) {
|
|
1193
|
+
console.error(`\nDone: 1 ${result}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function findGitRoot(startPath) {
|
|
1198
|
+
try {
|
|
1199
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
1200
|
+
cwd: startPath,
|
|
1201
|
+
encoding: 'utf-8',
|
|
1202
|
+
timeout: 2000,
|
|
1203
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1204
|
+
}).trim();
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function indexSingleFile(filePath, projectName) {
|
|
1211
|
+
const { parseFile } = await import('./lib/file-parser.js');
|
|
1212
|
+
const { indexFile } = await import('./lib/api-client.js');
|
|
1213
|
+
const { normalizePath } = await import('./lib/project-detector.js');
|
|
1214
|
+
const { relative, dirname } = await import('path');
|
|
1215
|
+
let parsed;
|
|
1216
|
+
try {
|
|
1217
|
+
parsed = parseFile(filePath);
|
|
1218
|
+
}
|
|
1219
|
+
catch (err) {
|
|
1220
|
+
const short = basename(filePath);
|
|
1221
|
+
console.error(` x ${short} - ${err.message}`);
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
// Build source_url as file:// relative to git root
|
|
1225
|
+
const gitRoot = findGitRoot(dirname(filePath)) || process.cwd();
|
|
1226
|
+
const relPath = normalizePath(relative(gitRoot, filePath));
|
|
1227
|
+
const sourceUrl = `file://${relPath}`;
|
|
1228
|
+
const shortPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
|
|
1229
|
+
try {
|
|
1230
|
+
const result = await indexFile({
|
|
1231
|
+
content: parsed.content,
|
|
1232
|
+
title: parsed.title,
|
|
1233
|
+
source_url: sourceUrl,
|
|
1234
|
+
file_type: parsed.file_type,
|
|
1235
|
+
project_name: projectName,
|
|
1236
|
+
tags: [...parsed.tags, 'indexed'],
|
|
1237
|
+
content_category: parsed.content_category,
|
|
1238
|
+
metadata: { word_count: parsed.word_count, file_type: parsed.file_type },
|
|
1239
|
+
});
|
|
1240
|
+
const status = result.status === 'updated' ? 'updated' : 'created';
|
|
1241
|
+
console.error(` ${status === 'updated' ? '~' : '+'} ${shortPath}`);
|
|
1242
|
+
return status;
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
console.error(` x ${shortPath} - ${err.message}`);
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function indexDirectory(dirPath, projectName) {
|
|
1250
|
+
const { readdirSync, statSync: fsStat } = await import('fs');
|
|
1251
|
+
const { join: pathJoin } = await import('path');
|
|
1252
|
+
const { isSupported, getExtension } = await import('./lib/file-parser.js');
|
|
1253
|
+
const SKIP_DIRS = new Set([
|
|
1254
|
+
'node_modules', '.git', 'dist', 'build', 'coverage',
|
|
1255
|
+
'.next', '__pycache__', '.turbo', '.cache', '.expo',
|
|
1256
|
+
'vendor', '.venv', 'venv', 'target', '.output',
|
|
1257
|
+
]);
|
|
1258
|
+
const MAX_FILES = 50;
|
|
1259
|
+
const files = [];
|
|
1260
|
+
function walk(dir) {
|
|
1261
|
+
if (files.length >= MAX_FILES)
|
|
1262
|
+
return;
|
|
1263
|
+
try {
|
|
1264
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1265
|
+
for (const entry of entries) {
|
|
1266
|
+
if (files.length >= MAX_FILES)
|
|
1267
|
+
return;
|
|
1268
|
+
if (entry.name.startsWith('.'))
|
|
1269
|
+
continue;
|
|
1270
|
+
const full = pathJoin(dir, entry.name);
|
|
1271
|
+
if (entry.isDirectory()) {
|
|
1272
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
1273
|
+
walk(full);
|
|
1274
|
+
}
|
|
1275
|
+
else if (isSupported(getExtension(entry.name))) {
|
|
1276
|
+
files.push(full);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
catch { /* skip unreadable dirs */ }
|
|
1281
|
+
}
|
|
1282
|
+
walk(dirPath);
|
|
1283
|
+
if (files.length === 0) {
|
|
1284
|
+
console.error('No supported files found.');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
console.error(`Indexing ${files.length} file(s)${files.length >= MAX_FILES ? ` (capped at ${MAX_FILES})` : ''}...\n`);
|
|
1288
|
+
let created = 0;
|
|
1289
|
+
let updated = 0;
|
|
1290
|
+
let failed = 0;
|
|
1291
|
+
// Process with concurrency limit of 5
|
|
1292
|
+
const CONCURRENCY = 5;
|
|
1293
|
+
for (let i = 0; i < files.length; i += CONCURRENCY) {
|
|
1294
|
+
const batch = files.slice(i, i + CONCURRENCY);
|
|
1295
|
+
const results = await Promise.allSettled(batch.map(f => indexSingleFile(f, projectName)));
|
|
1296
|
+
for (const r of results) {
|
|
1297
|
+
if (r.status === 'fulfilled' && r.value) {
|
|
1298
|
+
if (r.value === 'updated')
|
|
1299
|
+
updated++;
|
|
1300
|
+
else
|
|
1301
|
+
created++;
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
failed++;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
console.error(`\nDone: ${created} indexed, ${updated} updated, ${failed} failed`);
|
|
1309
|
+
}
|
|
1310
|
+
async function indexUrl(url, projectName) {
|
|
1311
|
+
const { saveFromAgent } = await import('./lib/api-client.js');
|
|
1312
|
+
console.error(`Indexing URL: ${url}`);
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await saveFromAgent({
|
|
1315
|
+
content: url,
|
|
1316
|
+
title: url,
|
|
1317
|
+
project_name: projectName,
|
|
1318
|
+
tags: ['indexed', 'url'],
|
|
1319
|
+
});
|
|
1320
|
+
if (result.duplicate) {
|
|
1321
|
+
console.error(' ~ Already indexed');
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
console.error(` + Saved`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch (err) {
|
|
1328
|
+
console.error(` x Failed: ${err.message}`);
|
|
1329
|
+
process.exit(1);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1145
1332
|
function printHelp() {
|
|
1146
1333
|
console.error(`
|
|
1147
1334
|
Splicr — route what you read to what you're building
|
|
@@ -1157,6 +1344,8 @@ function printHelp() {
|
|
|
1157
1344
|
team list List your teams
|
|
1158
1345
|
team invite Show invite link for your team
|
|
1159
1346
|
team join <code> Join a team by invite code
|
|
1347
|
+
index <path> Index local files into your knowledge base
|
|
1348
|
+
index <dir> Index all supported files in a directory
|
|
1160
1349
|
dashboard Open knowledge dashboard in browser
|
|
1161
1350
|
uninstall Remove Splicr from all coding agents
|
|
1162
1351
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ 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
|
+
}>;
|
|
39
53
|
export declare function resolveProject(params: {
|
|
40
54
|
local_path?: string;
|
|
41
55
|
git_remote_url?: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -58,6 +58,9 @@ 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
|
+
}
|
|
61
64
|
export async function resolveProject(params) {
|
|
62
65
|
return await apiRequest('POST', '/mcp/resolve-project', params);
|
|
63
66
|
}
|
|
@@ -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.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",
|
|
@@ -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"
|