@virsanghavi/axis-server 1.4.0 → 1.6.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 +228 -47
  2. package/package.json +1 -1
@@ -1166,6 +1166,7 @@ import fs4 from "fs";
1166
1166
 
1167
1167
  // ../../src/local/local-search.ts
1168
1168
  import fs3 from "fs/promises";
1169
+ import fsSync2 from "fs";
1169
1170
  import path3 from "path";
1170
1171
  var SKIP_DIRS = /* @__PURE__ */ new Set([
1171
1172
  "node_modules",
@@ -1321,34 +1322,61 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1321
1322
  "most",
1322
1323
  "some",
1323
1324
  "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
1325
  "there",
1338
1326
  "here",
1339
1327
  "just",
1340
1328
  "also",
1341
1329
  "very",
1342
1330
  "really",
1343
- "quite"
1331
+ "quite",
1332
+ "show",
1333
+ "look",
1334
+ "locate",
1335
+ "using",
1336
+ "used",
1337
+ "need",
1338
+ "want"
1344
1339
  ]);
1345
1340
  var MAX_FILE_SIZE = 256 * 1024;
1346
1341
  var MAX_RESULTS = 20;
1347
1342
  var CONTEXT_LINES = 2;
1348
1343
  var MAX_MATCHES_PER_FILE = 6;
1349
1344
  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)];
1345
+ const words = query.toLowerCase().replace(/[^\w\s\-_.]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
1346
+ const filtered = words.filter((w) => !STOP_WORDS.has(w));
1347
+ const result = filtered.length > 0 ? filtered : words.filter((w) => w.length >= 3);
1348
+ return [...new Set(result)];
1349
+ }
1350
+ var PROJECT_ROOT_MARKERS = [
1351
+ ".git",
1352
+ ".axis",
1353
+ "package.json",
1354
+ "Cargo.toml",
1355
+ "go.mod",
1356
+ "pyproject.toml",
1357
+ "setup.py",
1358
+ "Gemfile",
1359
+ "pom.xml",
1360
+ "tsconfig.json",
1361
+ ".cursorrules",
1362
+ "AGENTS.md"
1363
+ ];
1364
+ function detectProjectRoot(startDir) {
1365
+ let current = startDir;
1366
+ const root = path3.parse(current).root;
1367
+ while (current !== root) {
1368
+ for (const marker of PROJECT_ROOT_MARKERS) {
1369
+ try {
1370
+ fsSync2.accessSync(path3.join(current, marker));
1371
+ return current;
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ const parent = path3.dirname(current);
1376
+ if (parent === current) break;
1377
+ current = parent;
1378
+ }
1379
+ return startDir;
1352
1380
  }
1353
1381
  async function walkDir(dir, maxDepth = 12) {
1354
1382
  const results = [];
@@ -1428,8 +1456,12 @@ async function searchFile(filePath, rootDir, keywords) {
1428
1456
  return { filePath, relativePath, score, matchedKeywords, regions };
1429
1457
  }
1430
1458
  async function localSearch(query, rootDir) {
1431
- const cwd = rootDir || process.cwd();
1459
+ const rawCwd = rootDir || process.cwd();
1460
+ const cwd = detectProjectRoot(rawCwd);
1432
1461
  const keywords = extractKeywords(query);
1462
+ if (cwd !== rawCwd) {
1463
+ logger.info(`[localSearch] Detected project root: ${cwd} (CWD was: ${rawCwd})`);
1464
+ }
1433
1465
  if (keywords.length === 0) {
1434
1466
  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
1467
  }
@@ -1545,6 +1577,120 @@ var nerveCenter = new NerveCenter(manager, {
1545
1577
  projectName: process.env.PROJECT_NAME || "default"
1546
1578
  });
1547
1579
  logger.info("=== Axis MCP Server Initialized ===");
1580
+ var RECHECK_INTERVAL_MS = 30 * 60 * 1e3;
1581
+ var GRACE_PERIOD_MS = 5 * 60 * 1e3;
1582
+ var subscription = {
1583
+ checked: false,
1584
+ valid: true,
1585
+ // Assume valid until proven otherwise (for startup)
1586
+ plan: "unknown",
1587
+ reason: "",
1588
+ checkedAt: 0
1589
+ };
1590
+ async function verifySubscription() {
1591
+ if (!apiSecret) {
1592
+ const hasDirectSupabase = !useRemoteApiOnly && !!process.env.NEXT_PUBLIC_SUPABASE_URL && !!process.env.SUPABASE_SERVICE_ROLE_KEY;
1593
+ if (hasDirectSupabase) {
1594
+ subscription = { checked: true, valid: true, plan: "developer", reason: "Direct Supabase mode \u2014 no API key needed", checkedAt: Date.now() };
1595
+ logger.info("[subscription] Direct Supabase credentials found \u2014 developer mode, skipping verification");
1596
+ return subscription;
1597
+ }
1598
+ subscription = {
1599
+ checked: true,
1600
+ valid: false,
1601
+ plan: "none",
1602
+ reason: "no_api_key",
1603
+ checkedAt: Date.now()
1604
+ };
1605
+ logger.error("[subscription] No API key configured. Axis requires an API key from https://useaxis.dev/dashboard");
1606
+ return subscription;
1607
+ }
1608
+ const verifyUrl = apiUrl.endsWith("/v1") ? `${apiUrl}/verify` : `${apiUrl}/v1/verify`;
1609
+ logger.info(`[subscription] Verifying subscription at ${verifyUrl}`);
1610
+ const controller = new AbortController();
1611
+ const timeout = setTimeout(() => controller.abort(), 1e4);
1612
+ try {
1613
+ const response = await fetch(verifyUrl, {
1614
+ method: "GET",
1615
+ headers: {
1616
+ "Authorization": `Bearer ${apiSecret}`
1617
+ },
1618
+ signal: controller.signal
1619
+ });
1620
+ clearTimeout(timeout);
1621
+ const data = await response.json();
1622
+ logger.info(`[subscription] Verify response: ${JSON.stringify(data)}`);
1623
+ if (data.valid === true) {
1624
+ subscription = {
1625
+ checked: true,
1626
+ valid: true,
1627
+ plan: data.plan || "Pro",
1628
+ reason: "",
1629
+ checkedAt: Date.now(),
1630
+ validUntil: data.validUntil
1631
+ };
1632
+ } else {
1633
+ subscription = {
1634
+ checked: true,
1635
+ valid: false,
1636
+ plan: data.plan || "Free",
1637
+ reason: data.reason || "subscription_invalid",
1638
+ checkedAt: Date.now()
1639
+ };
1640
+ logger.warn(`[subscription] Subscription NOT valid: ${data.reason}`);
1641
+ }
1642
+ } catch (e) {
1643
+ clearTimeout(timeout);
1644
+ logger.warn(`[subscription] Verification failed (network): ${e.message}`);
1645
+ if (!subscription.checked) {
1646
+ subscription = {
1647
+ checked: true,
1648
+ valid: true,
1649
+ // Grace period
1650
+ plan: "unverified",
1651
+ reason: "Verification endpoint unreachable \u2014 grace period active",
1652
+ checkedAt: Date.now()
1653
+ };
1654
+ logger.warn("[subscription] First check failed \u2014 allowing grace period");
1655
+ }
1656
+ }
1657
+ return subscription;
1658
+ }
1659
+ function isSubscriptionStale() {
1660
+ return Date.now() - subscription.checkedAt > RECHECK_INTERVAL_MS;
1661
+ }
1662
+ function getSubscriptionBlockMessage() {
1663
+ if (subscription.reason === "no_api_key") {
1664
+ return [
1665
+ "\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",
1666
+ " Axis API key required",
1667
+ "",
1668
+ " No API key found. Axis requires an active subscription",
1669
+ " and a valid API key to operate.",
1670
+ "",
1671
+ " 1. Sign up or log in at https://useaxis.dev",
1672
+ " 2. Subscribe to Axis Pro",
1673
+ " 3. Generate an API key from the dashboard",
1674
+ " 4. Add AXIS_API_KEY to your mcp.json configuration",
1675
+ " 5. Restart your IDE",
1676
+ "\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"
1677
+ ].join("\n");
1678
+ }
1679
+ return [
1680
+ "\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",
1681
+ " Axis Pro subscription required",
1682
+ "",
1683
+ ` Status: ${subscription.reason || "subscription_expired"}`,
1684
+ ` Current plan: ${subscription.plan}`,
1685
+ "",
1686
+ " Your Axis Pro subscription has expired or is inactive.",
1687
+ " All Axis MCP tools are disabled until the subscription is renewed.",
1688
+ "",
1689
+ " \u2192 Renew at https://useaxis.dev/dashboard",
1690
+ " \u2192 After renewing, restart your IDE to re-verify.",
1691
+ "\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"
1692
+ ].join("\n");
1693
+ }
1548
1694
  var ragEngine;
1549
1695
  if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
1550
1696
  ragEngine = new RagEngine(
@@ -1558,7 +1704,7 @@ async function ensureFileSystem() {
1558
1704
  try {
1559
1705
  const fs5 = await import("fs/promises");
1560
1706
  const path5 = await import("path");
1561
- const fsSync2 = await import("fs");
1707
+ const fsSync3 = await import("fs");
1562
1708
  const cwd = process.cwd();
1563
1709
  logger.info(`Server CWD: ${cwd}`);
1564
1710
  const historyDir = path5.join(cwd, "history");
@@ -1567,7 +1713,7 @@ async function ensureFileSystem() {
1567
1713
  const axisDir = path5.join(cwd, ".axis");
1568
1714
  const axisInstructions = path5.join(axisDir, "instructions");
1569
1715
  const legacyInstructions = path5.join(cwd, "agent-instructions");
1570
- if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
1716
+ if (fsSync3.existsSync(legacyInstructions) && !fsSync3.existsSync(axisDir)) {
1571
1717
  logger.info("Using legacy agent-instructions directory");
1572
1718
  } else {
1573
1719
  await fs5.mkdir(axisInstructions, { recursive: true }).catch(() => {
@@ -1894,6 +2040,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1894
2040
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1895
2041
  const { name, arguments: args } = request.params;
1896
2042
  logger.info("Tool call", { name });
2043
+ if (isSubscriptionStale()) {
2044
+ await verifySubscription();
2045
+ }
2046
+ if (!subscription.valid) {
2047
+ logger.warn(`[subscription] Blocking tool call "${name}" \u2014 subscription invalid`);
2048
+ return {
2049
+ content: [{ type: "text", text: getSubscriptionBlockMessage() }],
2050
+ isError: true
2051
+ };
2052
+ }
1897
2053
  if (name === READ_CONTEXT_TOOL) {
1898
2054
  const filename = String(args?.filename);
1899
2055
  try {
@@ -1940,41 +2096,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1940
2096
  }
1941
2097
  if (name === SEARCH_CONTEXT_TOOL) {
1942
2098
  const query = String(args?.query);
1943
- let ragResults = null;
1944
- try {
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 {
1958
- }
1959
- }
1960
- let localResults = null;
2099
+ logger.info(`[search_codebase] Query: "${query}"`);
2100
+ let localResults = "";
1961
2101
  try {
1962
2102
  localResults = await localSearch(query);
2103
+ logger.info(`[search_codebase] Local search completed: ${localResults.length} chars`);
1963
2104
  } catch (e) {
1964
2105
  logger.warn(`[search_codebase] Local search error: ${e}`);
2106
+ localResults = "";
1965
2107
  }
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." }] };
2108
+ let ragResults = null;
2109
+ const RAG_TIMEOUT_MS = 3e3;
2110
+ try {
2111
+ const ragPromise = (async () => {
2112
+ try {
2113
+ const remote = await manager.searchContext(query, nerveCenter.currentProjectName);
2114
+ if (remote && !remote.includes("No results found") && remote.trim().length > 20) {
2115
+ return remote;
2116
+ }
2117
+ } catch {
2118
+ }
2119
+ if (ragEngine) {
2120
+ try {
2121
+ const results = await ragEngine.search(query);
2122
+ if (results.length > 0) return results.join("\n---\n");
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ return null;
2127
+ })();
2128
+ ragResults = await Promise.race([
2129
+ ragPromise,
2130
+ new Promise((resolve) => setTimeout(() => resolve(null), RAG_TIMEOUT_MS))
2131
+ ]);
2132
+ if (ragResults) {
2133
+ logger.info(`[search_codebase] RAG returned results (${ragResults.length} chars)`);
2134
+ }
2135
+ } catch {
1974
2136
  }
1975
- if (parts.length === 0) {
1976
- return { content: [{ type: "text", text: "No results found for this query." }] };
2137
+ const hasLocal = localResults && !localResults.startsWith("No matches found") && !localResults.startsWith("Could not extract");
2138
+ if (!hasLocal && !ragResults) {
2139
+ return { content: [{ type: "text", text: localResults || "No results found for this query." }] };
1977
2140
  }
2141
+ const parts = [];
2142
+ if (hasLocal) parts.push(localResults);
2143
+ if (ragResults) parts.push("## Indexed Results (RAG)\n\n" + ragResults);
1978
2144
  return { content: [{ type: "text", text: parts.join("\n\n---\n\n") }] };
1979
2145
  }
1980
2146
  if (name === "get_subscription_status") {
@@ -2069,6 +2235,21 @@ async function main() {
2069
2235
  ragEngine.setProjectId(nerveCenter.projectId);
2070
2236
  logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
2071
2237
  }
2238
+ await verifySubscription();
2239
+ if (!subscription.valid) {
2240
+ logger.error("[subscription] Subscription invalid at startup \u2014 all tools will be blocked");
2241
+ logger.error(`[subscription] Reason: ${subscription.reason} | Plan: ${subscription.plan}`);
2242
+ } else {
2243
+ logger.info(`[subscription] Subscription verified: ${subscription.plan} (valid until: ${subscription.validUntil || "N/A"})`);
2244
+ }
2245
+ setInterval(async () => {
2246
+ try {
2247
+ await verifySubscription();
2248
+ logger.info(`[subscription] Periodic re-check: valid=${subscription.valid}, plan=${subscription.plan}`);
2249
+ } catch (e) {
2250
+ logger.warn(`[subscription] Periodic re-check failed: ${e}`);
2251
+ }
2252
+ }, RECHECK_INTERVAL_MS);
2072
2253
  logger.info("MCP server ready - all tools and resources registered");
2073
2254
  const transport = new StdioServerTransport();
2074
2255
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {