@virsanghavi/axis-server 1.3.0 → 1.4.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/mcp-server.mjs +367 -26
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -1161,23 +1161,338 @@ var RagEngine = class {
|
|
|
1161
1161
|
};
|
|
1162
1162
|
|
|
1163
1163
|
// ../../src/local/mcp-server.ts
|
|
1164
|
+
import path4 from "path";
|
|
1165
|
+
import fs4 from "fs";
|
|
1166
|
+
|
|
1167
|
+
// ../../src/local/local-search.ts
|
|
1168
|
+
import fs3 from "fs/promises";
|
|
1164
1169
|
import path3 from "path";
|
|
1165
|
-
|
|
1170
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1171
|
+
"node_modules",
|
|
1172
|
+
".git",
|
|
1173
|
+
".next",
|
|
1174
|
+
".nuxt",
|
|
1175
|
+
".svelte-kit",
|
|
1176
|
+
"dist",
|
|
1177
|
+
"build",
|
|
1178
|
+
"out",
|
|
1179
|
+
".output",
|
|
1180
|
+
"coverage",
|
|
1181
|
+
"__pycache__",
|
|
1182
|
+
".pytest_cache",
|
|
1183
|
+
".mypy_cache",
|
|
1184
|
+
".venv",
|
|
1185
|
+
"venv",
|
|
1186
|
+
"env",
|
|
1187
|
+
".turbo",
|
|
1188
|
+
".cache",
|
|
1189
|
+
".parcel-cache",
|
|
1190
|
+
".axis",
|
|
1191
|
+
"history",
|
|
1192
|
+
".DS_Store"
|
|
1193
|
+
]);
|
|
1194
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1195
|
+
// Binary / media
|
|
1196
|
+
".png",
|
|
1197
|
+
".jpg",
|
|
1198
|
+
".jpeg",
|
|
1199
|
+
".gif",
|
|
1200
|
+
".webp",
|
|
1201
|
+
".ico",
|
|
1202
|
+
".svg",
|
|
1203
|
+
".mp3",
|
|
1204
|
+
".mp4",
|
|
1205
|
+
".wav",
|
|
1206
|
+
".webm",
|
|
1207
|
+
".ogg",
|
|
1208
|
+
".woff",
|
|
1209
|
+
".woff2",
|
|
1210
|
+
".ttf",
|
|
1211
|
+
".eot",
|
|
1212
|
+
".pdf",
|
|
1213
|
+
".zip",
|
|
1214
|
+
".tar",
|
|
1215
|
+
".gz",
|
|
1216
|
+
".br",
|
|
1217
|
+
// Compiled / generated
|
|
1218
|
+
".pyc",
|
|
1219
|
+
".pyo",
|
|
1220
|
+
".so",
|
|
1221
|
+
".dylib",
|
|
1222
|
+
".dll",
|
|
1223
|
+
".exe",
|
|
1224
|
+
".class",
|
|
1225
|
+
".jar",
|
|
1226
|
+
".war",
|
|
1227
|
+
".wasm",
|
|
1228
|
+
// Lock files (huge, not useful for search)
|
|
1229
|
+
".lock"
|
|
1230
|
+
]);
|
|
1231
|
+
var SKIP_FILENAMES = /* @__PURE__ */ new Set([
|
|
1232
|
+
"package-lock.json",
|
|
1233
|
+
"yarn.lock",
|
|
1234
|
+
"pnpm-lock.yaml",
|
|
1235
|
+
"Cargo.lock",
|
|
1236
|
+
"Gemfile.lock",
|
|
1237
|
+
"poetry.lock",
|
|
1238
|
+
".DS_Store",
|
|
1239
|
+
"Thumbs.db"
|
|
1240
|
+
]);
|
|
1241
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1242
|
+
"a",
|
|
1243
|
+
"an",
|
|
1244
|
+
"the",
|
|
1245
|
+
"is",
|
|
1246
|
+
"are",
|
|
1247
|
+
"was",
|
|
1248
|
+
"were",
|
|
1249
|
+
"be",
|
|
1250
|
+
"been",
|
|
1251
|
+
"being",
|
|
1252
|
+
"have",
|
|
1253
|
+
"has",
|
|
1254
|
+
"had",
|
|
1255
|
+
"do",
|
|
1256
|
+
"does",
|
|
1257
|
+
"did",
|
|
1258
|
+
"will",
|
|
1259
|
+
"would",
|
|
1260
|
+
"could",
|
|
1261
|
+
"should",
|
|
1262
|
+
"may",
|
|
1263
|
+
"might",
|
|
1264
|
+
"shall",
|
|
1265
|
+
"can",
|
|
1266
|
+
"i",
|
|
1267
|
+
"me",
|
|
1268
|
+
"my",
|
|
1269
|
+
"we",
|
|
1270
|
+
"our",
|
|
1271
|
+
"you",
|
|
1272
|
+
"your",
|
|
1273
|
+
"he",
|
|
1274
|
+
"she",
|
|
1275
|
+
"it",
|
|
1276
|
+
"they",
|
|
1277
|
+
"them",
|
|
1278
|
+
"their",
|
|
1279
|
+
"this",
|
|
1280
|
+
"that",
|
|
1281
|
+
"these",
|
|
1282
|
+
"those",
|
|
1283
|
+
"what",
|
|
1284
|
+
"which",
|
|
1285
|
+
"who",
|
|
1286
|
+
"whom",
|
|
1287
|
+
"where",
|
|
1288
|
+
"when",
|
|
1289
|
+
"how",
|
|
1290
|
+
"why",
|
|
1291
|
+
"in",
|
|
1292
|
+
"on",
|
|
1293
|
+
"at",
|
|
1294
|
+
"to",
|
|
1295
|
+
"for",
|
|
1296
|
+
"of",
|
|
1297
|
+
"with",
|
|
1298
|
+
"by",
|
|
1299
|
+
"from",
|
|
1300
|
+
"up",
|
|
1301
|
+
"about",
|
|
1302
|
+
"into",
|
|
1303
|
+
"through",
|
|
1304
|
+
"during",
|
|
1305
|
+
"before",
|
|
1306
|
+
"after",
|
|
1307
|
+
"and",
|
|
1308
|
+
"but",
|
|
1309
|
+
"or",
|
|
1310
|
+
"nor",
|
|
1311
|
+
"not",
|
|
1312
|
+
"so",
|
|
1313
|
+
"if",
|
|
1314
|
+
"then",
|
|
1315
|
+
"all",
|
|
1316
|
+
"each",
|
|
1317
|
+
"every",
|
|
1318
|
+
"both",
|
|
1319
|
+
"few",
|
|
1320
|
+
"more",
|
|
1321
|
+
"most",
|
|
1322
|
+
"some",
|
|
1323
|
+
"any",
|
|
1324
|
+
"find",
|
|
1325
|
+
"show",
|
|
1326
|
+
"get",
|
|
1327
|
+
"look",
|
|
1328
|
+
"search",
|
|
1329
|
+
"locate",
|
|
1330
|
+
"check",
|
|
1331
|
+
"file",
|
|
1332
|
+
"files",
|
|
1333
|
+
"code",
|
|
1334
|
+
"function",
|
|
1335
|
+
"class",
|
|
1336
|
+
"method",
|
|
1337
|
+
"there",
|
|
1338
|
+
"here",
|
|
1339
|
+
"just",
|
|
1340
|
+
"also",
|
|
1341
|
+
"very",
|
|
1342
|
+
"really",
|
|
1343
|
+
"quite"
|
|
1344
|
+
]);
|
|
1345
|
+
var MAX_FILE_SIZE = 256 * 1024;
|
|
1346
|
+
var MAX_RESULTS = 20;
|
|
1347
|
+
var CONTEXT_LINES = 2;
|
|
1348
|
+
var MAX_MATCHES_PER_FILE = 6;
|
|
1349
|
+
function extractKeywords(query) {
|
|
1350
|
+
const raw = query.toLowerCase().replace(/[^\w\s\-_.]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !STOP_WORDS.has(w));
|
|
1351
|
+
return [...new Set(raw)];
|
|
1352
|
+
}
|
|
1353
|
+
async function walkDir(dir, maxDepth = 12) {
|
|
1354
|
+
const results = [];
|
|
1355
|
+
async function recurse(current, depth) {
|
|
1356
|
+
if (depth > maxDepth) return;
|
|
1357
|
+
let entries;
|
|
1358
|
+
try {
|
|
1359
|
+
entries = await fs3.readdir(current, { withFileTypes: true });
|
|
1360
|
+
} catch {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
for (const entry of entries) {
|
|
1364
|
+
if (entry.name.startsWith(".") && entry.name !== ".env.example") {
|
|
1365
|
+
if (SKIP_DIRS.has(entry.name) || entry.isDirectory()) continue;
|
|
1366
|
+
}
|
|
1367
|
+
const fullPath = path3.join(current, entry.name);
|
|
1368
|
+
if (entry.isDirectory()) {
|
|
1369
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1370
|
+
await recurse(fullPath, depth + 1);
|
|
1371
|
+
} else if (entry.isFile()) {
|
|
1372
|
+
if (SKIP_FILENAMES.has(entry.name)) continue;
|
|
1373
|
+
const ext = path3.extname(entry.name).toLowerCase();
|
|
1374
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
1375
|
+
try {
|
|
1376
|
+
const stat = await fs3.stat(fullPath);
|
|
1377
|
+
if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
|
|
1378
|
+
} catch {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
results.push(fullPath);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
await recurse(dir, 0);
|
|
1386
|
+
return results;
|
|
1387
|
+
}
|
|
1388
|
+
async function searchFile(filePath, rootDir, keywords) {
|
|
1389
|
+
let content;
|
|
1390
|
+
try {
|
|
1391
|
+
content = await fs3.readFile(filePath, "utf-8");
|
|
1392
|
+
} catch {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
const contentLower = content.toLowerCase();
|
|
1396
|
+
const relativePath = path3.relative(rootDir, filePath);
|
|
1397
|
+
const matchedKeywords = keywords.filter((kw) => contentLower.includes(kw));
|
|
1398
|
+
if (matchedKeywords.length === 0) return null;
|
|
1399
|
+
const lines = content.split("\n");
|
|
1400
|
+
let score = matchedKeywords.length;
|
|
1401
|
+
const relLower = relativePath.toLowerCase();
|
|
1402
|
+
for (const kw of keywords) {
|
|
1403
|
+
if (relLower.includes(kw)) score += 2;
|
|
1404
|
+
}
|
|
1405
|
+
const matchingLineIndices = [];
|
|
1406
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1407
|
+
const lineLower = lines[i].toLowerCase();
|
|
1408
|
+
if (matchedKeywords.some((kw) => lineLower.includes(kw))) {
|
|
1409
|
+
matchingLineIndices.push(i);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
score += Math.min(matchingLineIndices.length, 20) * 0.1;
|
|
1413
|
+
const regions = [];
|
|
1414
|
+
let lastEnd = -1;
|
|
1415
|
+
for (const idx of matchingLineIndices) {
|
|
1416
|
+
if (regions.length >= MAX_MATCHES_PER_FILE) break;
|
|
1417
|
+
const start = Math.max(0, idx - CONTEXT_LINES);
|
|
1418
|
+
const end = Math.min(lines.length - 1, idx + CONTEXT_LINES);
|
|
1419
|
+
if (start <= lastEnd) continue;
|
|
1420
|
+
const regionLines = lines.slice(start, end + 1).map((line, i) => {
|
|
1421
|
+
const lineNum = start + i + 1;
|
|
1422
|
+
const marker = start + i === idx ? ">" : " ";
|
|
1423
|
+
return `${marker} ${lineNum.toString().padStart(4)}| ${line}`;
|
|
1424
|
+
}).join("\n");
|
|
1425
|
+
regions.push({ lineNumber: idx + 1, lines: regionLines });
|
|
1426
|
+
lastEnd = end;
|
|
1427
|
+
}
|
|
1428
|
+
return { filePath, relativePath, score, matchedKeywords, regions };
|
|
1429
|
+
}
|
|
1430
|
+
async function localSearch(query, rootDir) {
|
|
1431
|
+
const cwd = rootDir || process.cwd();
|
|
1432
|
+
const keywords = extractKeywords(query);
|
|
1433
|
+
if (keywords.length === 0) {
|
|
1434
|
+
return "Could not extract meaningful search terms from the query. Try being more specific (e.g. 'authentication middleware' instead of 'how does it work').";
|
|
1435
|
+
}
|
|
1436
|
+
logger.info(`[localSearch] Query: "${query}" \u2192 Keywords: [${keywords.join(", ")}] in ${cwd}`);
|
|
1437
|
+
const files = await walkDir(cwd);
|
|
1438
|
+
logger.info(`[localSearch] Scanning ${files.length} files`);
|
|
1439
|
+
const BATCH_SIZE = 50;
|
|
1440
|
+
const allMatches = [];
|
|
1441
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
1442
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
1443
|
+
const results = await Promise.all(
|
|
1444
|
+
batch.map((f) => searchFile(f, cwd, keywords))
|
|
1445
|
+
);
|
|
1446
|
+
for (const r of results) {
|
|
1447
|
+
if (r) allMatches.push(r);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
allMatches.sort((a, b) => b.score - a.score);
|
|
1451
|
+
const topMatches = allMatches.slice(0, MAX_RESULTS);
|
|
1452
|
+
if (topMatches.length === 0) {
|
|
1453
|
+
return `No matches found for: "${query}" (searched ${files.length} files for keywords: ${keywords.join(", ")}).
|
|
1454
|
+
Try different terms or check if the code exists in this project.`;
|
|
1455
|
+
}
|
|
1456
|
+
let output = `Found ${allMatches.length} matching file${allMatches.length === 1 ? "" : "s"} (showing top ${topMatches.length}, searched ${files.length} files)
|
|
1457
|
+
`;
|
|
1458
|
+
output += `Keywords: ${keywords.join(", ")}
|
|
1459
|
+
`;
|
|
1460
|
+
output += "\u2550".repeat(60) + "\n\n";
|
|
1461
|
+
for (const match of topMatches) {
|
|
1462
|
+
output += `\u{1F4C4} ${match.relativePath}
|
|
1463
|
+
`;
|
|
1464
|
+
output += ` Keywords matched: ${match.matchedKeywords.join(", ")} | Score: ${match.score.toFixed(1)}
|
|
1465
|
+
`;
|
|
1466
|
+
if (match.regions.length > 0) {
|
|
1467
|
+
output += " \u2500\u2500\u2500\u2500\u2500\n";
|
|
1468
|
+
for (const region of match.regions) {
|
|
1469
|
+
output += region.lines.split("\n").map((l) => ` ${l}`).join("\n") + "\n";
|
|
1470
|
+
if (region !== match.regions[match.regions.length - 1]) {
|
|
1471
|
+
output += " ...\n";
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
output += "\n";
|
|
1476
|
+
}
|
|
1477
|
+
return output;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// ../../src/local/mcp-server.ts
|
|
1166
1481
|
if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
|
|
1167
1482
|
logger.info("Using configuration from MCP client (mcp.json)");
|
|
1168
1483
|
} else {
|
|
1169
1484
|
const cwd = process.cwd();
|
|
1170
1485
|
const possiblePaths = [
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1486
|
+
path4.join(cwd, ".env.local"),
|
|
1487
|
+
path4.join(cwd, "..", ".env.local"),
|
|
1488
|
+
path4.join(cwd, "..", "..", ".env.local"),
|
|
1489
|
+
path4.join(cwd, "shared-context", ".env.local"),
|
|
1490
|
+
path4.join(cwd, "..", "shared-context", ".env.local")
|
|
1176
1491
|
];
|
|
1177
1492
|
let envLoaded = false;
|
|
1178
1493
|
for (const envPath of possiblePaths) {
|
|
1179
1494
|
try {
|
|
1180
|
-
if (
|
|
1495
|
+
if (fs4.existsSync(envPath)) {
|
|
1181
1496
|
logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
|
|
1182
1497
|
dotenv2.config({ path: envPath });
|
|
1183
1498
|
envLoaded = true;
|
|
@@ -1241,21 +1556,21 @@ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUP
|
|
|
1241
1556
|
}
|
|
1242
1557
|
async function ensureFileSystem() {
|
|
1243
1558
|
try {
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1559
|
+
const fs5 = await import("fs/promises");
|
|
1560
|
+
const path5 = await import("path");
|
|
1246
1561
|
const fsSync2 = await import("fs");
|
|
1247
1562
|
const cwd = process.cwd();
|
|
1248
1563
|
logger.info(`Server CWD: ${cwd}`);
|
|
1249
|
-
const historyDir =
|
|
1250
|
-
await
|
|
1564
|
+
const historyDir = path5.join(cwd, "history");
|
|
1565
|
+
await fs5.mkdir(historyDir, { recursive: true }).catch(() => {
|
|
1251
1566
|
});
|
|
1252
|
-
const axisDir =
|
|
1253
|
-
const axisInstructions =
|
|
1254
|
-
const legacyInstructions =
|
|
1567
|
+
const axisDir = path5.join(cwd, ".axis");
|
|
1568
|
+
const axisInstructions = path5.join(axisDir, "instructions");
|
|
1569
|
+
const legacyInstructions = path5.join(cwd, "agent-instructions");
|
|
1255
1570
|
if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
|
|
1256
1571
|
logger.info("Using legacy agent-instructions directory");
|
|
1257
1572
|
} else {
|
|
1258
|
-
await
|
|
1573
|
+
await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
|
|
1259
1574
|
});
|
|
1260
1575
|
const defaults = [
|
|
1261
1576
|
["context.md", `# Project Context
|
|
@@ -1310,11 +1625,11 @@ force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed a
|
|
|
1310
1625
|
["activity.md", "# Activity Log\n\n"]
|
|
1311
1626
|
];
|
|
1312
1627
|
for (const [file, content] of defaults) {
|
|
1313
|
-
const p =
|
|
1628
|
+
const p = path5.join(axisInstructions, file);
|
|
1314
1629
|
try {
|
|
1315
|
-
await
|
|
1630
|
+
await fs5.access(p);
|
|
1316
1631
|
} catch {
|
|
1317
|
-
await
|
|
1632
|
+
await fs5.writeFile(p, content);
|
|
1318
1633
|
logger.info(`Created default context file: ${file}`);
|
|
1319
1634
|
}
|
|
1320
1635
|
}
|
|
@@ -1415,7 +1730,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1415
1730
|
},
|
|
1416
1731
|
{
|
|
1417
1732
|
name: SEARCH_CONTEXT_TOOL,
|
|
1418
|
-
description: "**
|
|
1733
|
+
description: "**CODEBASE SEARCH** \u2014 search the entire project by natural language or keywords.\n- Scans all source files on disk. Always returns results if matching code exists \u2014 no setup required.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the database connection code'.\n- Also checks the RAG vector index if available, but the local filesystem search always works.\n- Use this INSTEAD of grep/ripgrep to stay within the Axis workflow. This tool searches file contents directly.",
|
|
1419
1734
|
inputSchema: {
|
|
1420
1735
|
type: "object",
|
|
1421
1736
|
properties: {
|
|
@@ -1625,16 +1940,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1625
1940
|
}
|
|
1626
1941
|
if (name === SEARCH_CONTEXT_TOOL) {
|
|
1627
1942
|
const query = String(args?.query);
|
|
1943
|
+
let ragResults = null;
|
|
1628
1944
|
try {
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1945
|
+
const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
|
|
1946
|
+
if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
|
|
1947
|
+
ragResults = remote;
|
|
1948
|
+
}
|
|
1949
|
+
} catch {
|
|
1950
|
+
}
|
|
1951
|
+
if (!ragResults && ragEngine) {
|
|
1952
|
+
try {
|
|
1953
|
+
const localRag = await ragEngine.search(query);
|
|
1954
|
+
if (localRag.length > 0) {
|
|
1955
|
+
ragResults = localRag.join("\n---\n");
|
|
1956
|
+
}
|
|
1957
|
+
} catch {
|
|
1635
1958
|
}
|
|
1636
|
-
return { content: [{ type: "text", text: `Search failed: ${e}` }], isError: true };
|
|
1637
1959
|
}
|
|
1960
|
+
let localResults = null;
|
|
1961
|
+
try {
|
|
1962
|
+
localResults = await localSearch(query);
|
|
1963
|
+
} catch (e) {
|
|
1964
|
+
logger.warn(`[search_codebase] Local search error: ${e}`);
|
|
1965
|
+
}
|
|
1966
|
+
const parts = [];
|
|
1967
|
+
if (ragResults) {
|
|
1968
|
+
parts.push("## Indexed Results (RAG)\n\n" + ragResults);
|
|
1969
|
+
}
|
|
1970
|
+
if (localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract")) {
|
|
1971
|
+
parts.push("## Local Codebase Search\n\n" + localResults);
|
|
1972
|
+
} else if (!ragResults) {
|
|
1973
|
+
return { content: [{ type: "text", text: localResults || "No results found." }] };
|
|
1974
|
+
}
|
|
1975
|
+
if (parts.length === 0) {
|
|
1976
|
+
return { content: [{ type: "text", text: "No results found for this query." }] };
|
|
1977
|
+
}
|
|
1978
|
+
return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
|
|
1638
1979
|
}
|
|
1639
1980
|
if (name === "get_subscription_status") {
|
|
1640
1981
|
const email = String(args?.email);
|