@virsanghavi/axis-server 1.3.0 → 1.5.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.
Files changed (2) hide show
  1. package/dist/mcp-server.mjs +478 -26
  2. package/package.json +1 -1
@@ -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
- import fs3 from "fs";
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
- path3.join(cwd, ".env.local"),
1172
- path3.join(cwd, "..", ".env.local"),
1173
- path3.join(cwd, "..", "..", ".env.local"),
1174
- path3.join(cwd, "shared-context", ".env.local"),
1175
- path3.join(cwd, "..", "shared-context", ".env.local")
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 (fs3.existsSync(envPath)) {
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;
@@ -1230,6 +1545,92 @@ var nerveCenter = new NerveCenter(manager, {
1230
1545
  projectName: process.env.PROJECT_NAME || "default"
1231
1546
  });
1232
1547
  logger.info("=== Axis MCP Server Initialized ===");
1548
+ var RECHECK_INTERVAL_MS = 30 * 60 * 1e3;
1549
+ var GRACE_PERIOD_MS = 5 * 60 * 1e3;
1550
+ var subscription = {
1551
+ checked: false,
1552
+ valid: true,
1553
+ // Assume valid until proven otherwise (for startup)
1554
+ plan: "unknown",
1555
+ reason: "",
1556
+ checkedAt: 0
1557
+ };
1558
+ async function verifySubscription() {
1559
+ if (!apiSecret) {
1560
+ subscription = { checked: true, valid: true, plan: "local", reason: "No API key configured \u2014 local mode", checkedAt: Date.now() };
1561
+ logger.info("[subscription] No API key \u2014 running in local/dev mode, skipping verification");
1562
+ return subscription;
1563
+ }
1564
+ const verifyUrl = apiUrl.endsWith("/v1") ? `${apiUrl}/verify` : `${apiUrl}/v1/verify`;
1565
+ logger.info(`[subscription] Verifying subscription at ${verifyUrl}`);
1566
+ const controller = new AbortController();
1567
+ const timeout = setTimeout(() => controller.abort(), 1e4);
1568
+ try {
1569
+ const response = await fetch(verifyUrl, {
1570
+ method: "GET",
1571
+ headers: {
1572
+ "Authorization": `Bearer ${apiSecret}`
1573
+ },
1574
+ signal: controller.signal
1575
+ });
1576
+ clearTimeout(timeout);
1577
+ const data = await response.json();
1578
+ logger.info(`[subscription] Verify response: ${JSON.stringify(data)}`);
1579
+ if (data.valid === true) {
1580
+ subscription = {
1581
+ checked: true,
1582
+ valid: true,
1583
+ plan: data.plan || "Pro",
1584
+ reason: "",
1585
+ checkedAt: Date.now(),
1586
+ validUntil: data.validUntil
1587
+ };
1588
+ } else {
1589
+ subscription = {
1590
+ checked: true,
1591
+ valid: false,
1592
+ plan: data.plan || "Free",
1593
+ reason: data.reason || "subscription_invalid",
1594
+ checkedAt: Date.now()
1595
+ };
1596
+ logger.warn(`[subscription] Subscription NOT valid: ${data.reason}`);
1597
+ }
1598
+ } catch (e) {
1599
+ clearTimeout(timeout);
1600
+ logger.warn(`[subscription] Verification failed (network): ${e.message}`);
1601
+ if (!subscription.checked) {
1602
+ subscription = {
1603
+ checked: true,
1604
+ valid: true,
1605
+ // Grace period
1606
+ plan: "unverified",
1607
+ reason: "Verification endpoint unreachable \u2014 grace period active",
1608
+ checkedAt: Date.now()
1609
+ };
1610
+ logger.warn("[subscription] First check failed \u2014 allowing grace period");
1611
+ }
1612
+ }
1613
+ return subscription;
1614
+ }
1615
+ function isSubscriptionStale() {
1616
+ return Date.now() - subscription.checkedAt > RECHECK_INTERVAL_MS;
1617
+ }
1618
+ function getSubscriptionBlockMessage() {
1619
+ return [
1620
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
1621
+ " Axis Pro subscription required",
1622
+ "",
1623
+ ` Status: ${subscription.reason || "subscription_expired"}`,
1624
+ ` Current plan: ${subscription.plan}`,
1625
+ "",
1626
+ " Your Axis Pro subscription has expired or is inactive.",
1627
+ " All Axis MCP tools are disabled until the subscription is renewed.",
1628
+ "",
1629
+ " \u2192 Renew at https://useaxis.dev/dashboard",
1630
+ " \u2192 After renewing, restart your IDE to re-verify.",
1631
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
1632
+ ].join("\n");
1633
+ }
1233
1634
  var ragEngine;
1234
1635
  if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
1235
1636
  ragEngine = new RagEngine(
@@ -1241,21 +1642,21 @@ if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUP
1241
1642
  }
1242
1643
  async function ensureFileSystem() {
1243
1644
  try {
1244
- const fs4 = await import("fs/promises");
1245
- const path4 = await import("path");
1645
+ const fs5 = await import("fs/promises");
1646
+ const path5 = await import("path");
1246
1647
  const fsSync2 = await import("fs");
1247
1648
  const cwd = process.cwd();
1248
1649
  logger.info(`Server CWD: ${cwd}`);
1249
- const historyDir = path4.join(cwd, "history");
1250
- await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
1650
+ const historyDir = path5.join(cwd, "history");
1651
+ await fs5.mkdir(historyDir, { recursive: true }).catch(() => {
1251
1652
  });
1252
- const axisDir = path4.join(cwd, ".axis");
1253
- const axisInstructions = path4.join(axisDir, "instructions");
1254
- const legacyInstructions = path4.join(cwd, "agent-instructions");
1653
+ const axisDir = path5.join(cwd, ".axis");
1654
+ const axisInstructions = path5.join(axisDir, "instructions");
1655
+ const legacyInstructions = path5.join(cwd, "agent-instructions");
1255
1656
  if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
1256
1657
  logger.info("Using legacy agent-instructions directory");
1257
1658
  } else {
1258
- await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
1659
+ await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
1259
1660
  });
1260
1661
  const defaults = [
1261
1662
  ["context.md", `# Project Context
@@ -1310,11 +1711,11 @@ force_unlock is a LAST RESORT \u2014 only for locks >25 min old from a crashed a
1310
1711
  ["activity.md", "# Activity Log\n\n"]
1311
1712
  ];
1312
1713
  for (const [file, content] of defaults) {
1313
- const p = path4.join(axisInstructions, file);
1714
+ const p = path5.join(axisInstructions, file);
1314
1715
  try {
1315
- await fs4.access(p);
1716
+ await fs5.access(p);
1316
1717
  } catch {
1317
- await fs4.writeFile(p, content);
1718
+ await fs5.writeFile(p, content);
1318
1719
  logger.info(`Created default context file: ${file}`);
1319
1720
  }
1320
1721
  }
@@ -1415,7 +1816,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1415
1816
  },
1416
1817
  {
1417
1818
  name: SEARCH_CONTEXT_TOOL,
1418
- description: "**SEMANTIC SEARCH** for the codebase.\n- Uses vector similarity to find relevant code snippets or documentation.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the class that manages locks'.\n- Note: This searches *indexed* content only. For exact string matches, use `grep` (if available) or `warpgrep`.",
1819
+ 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
1820
  inputSchema: {
1420
1821
  type: "object",
1421
1822
  properties: {
@@ -1579,6 +1980,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1579
1980
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1580
1981
  const { name, arguments: args } = request.params;
1581
1982
  logger.info("Tool call", { name });
1983
+ if (isSubscriptionStale()) {
1984
+ await verifySubscription();
1985
+ }
1986
+ if (!subscription.valid) {
1987
+ logger.warn(`[subscription] Blocking tool call "${name}" \u2014 subscription invalid`);
1988
+ return {
1989
+ content: [{ type: "text", text: getSubscriptionBlockMessage() }],
1990
+ isError: true
1991
+ };
1992
+ }
1582
1993
  if (name === READ_CONTEXT_TOOL) {
1583
1994
  const filename = String(args?.filename);
1584
1995
  try {
@@ -1625,16 +2036,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1625
2036
  }
1626
2037
  if (name === SEARCH_CONTEXT_TOOL) {
1627
2038
  const query = String(args?.query);
2039
+ let ragResults = null;
1628
2040
  try {
1629
- const results = await manager.searchContext(query, nerveCenter.currentProjectName);
1630
- return { content: [{ type: "text", text: results }] };
1631
- } catch (e) {
1632
- if (ragEngine) {
1633
- const results = await ragEngine.search(query);
1634
- return { content: [{ type: "text", text: results.join("\n---\n") }] };
2041
+ const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
2042
+ if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
2043
+ ragResults = remote;
1635
2044
  }
1636
- return { content: [{ type: "text", text: `Search failed: ${e}` }], isError: true };
2045
+ } catch {
2046
+ }
2047
+ if (!ragResults && ragEngine) {
2048
+ try {
2049
+ const localRag = await ragEngine.search(query);
2050
+ if (localRag.length > 0) {
2051
+ ragResults = localRag.join("\n---\n");
2052
+ }
2053
+ } catch {
2054
+ }
2055
+ }
2056
+ let localResults = null;
2057
+ try {
2058
+ localResults = await localSearch(query);
2059
+ } catch (e) {
2060
+ logger.warn(`[search_codebase] Local search error: ${e}`);
2061
+ }
2062
+ const parts = [];
2063
+ if (ragResults) {
2064
+ parts.push("## Indexed Results (RAG)\n\n" + ragResults);
2065
+ }
2066
+ if (localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract")) {
2067
+ parts.push("## Local Codebase Search\n\n" + localResults);
2068
+ } else if (!ragResults) {
2069
+ return { content: [{ type: "text", text: localResults || "No results found." }] };
2070
+ }
2071
+ if (parts.length === 0) {
2072
+ return { content: [{ type: "text", text: "No results found for this query." }] };
1637
2073
  }
2074
+ return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
1638
2075
  }
1639
2076
  if (name === "get_subscription_status") {
1640
2077
  const email = String(args?.email);
@@ -1728,6 +2165,21 @@ async function main() {
1728
2165
  ragEngine.setProjectId(nerveCenter.projectId);
1729
2166
  logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
1730
2167
  }
2168
+ await verifySubscription();
2169
+ if (!subscription.valid) {
2170
+ logger.error("[subscription] Subscription invalid at startup \u2014 all tools will be blocked");
2171
+ logger.error(`[subscription] Reason: ${subscription.reason} | Plan: ${subscription.plan}`);
2172
+ } else {
2173
+ logger.info(`[subscription] Subscription verified: ${subscription.plan} (valid until: ${subscription.validUntil || "N/A"})`);
2174
+ }
2175
+ setInterval(async () => {
2176
+ try {
2177
+ await verifySubscription();
2178
+ logger.info(`[subscription] Periodic re-check: valid=${subscription.valid}, plan=${subscription.plan}`);
2179
+ } catch (e) {
2180
+ logger.warn(`[subscription] Periodic re-check failed: ${e}`);
2181
+ }
2182
+ }, RECHECK_INTERVAL_MS);
1731
2183
  logger.info("MCP server ready - all tools and resources registered");
1732
2184
  const transport = new StdioServerTransport();
1733
2185
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {