@adityanair98/api-oracle 0.5.0 → 0.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.
@@ -1,105 +1,108 @@
1
1
  /**
2
- * Search layer — combines SQLite text search with the scoring engine.
3
- * Uses synonym expansion and category detection to improve result quality.
2
+ * Search layer — scores the knowledge base against a task and keeps only
3
+ * candidates that are actually relevant to it.
4
4
  *
5
- * Exports: SearchConstraints, RankedResult, findApis, getApiBySlug, getApisByCategory
5
+ * Exports: SearchConstraints, RankedResult, SearchOutcome, RELEVANCE_GATE,
6
+ * findApis, findApisDetailed, getApiBySlug, getApisByCategory
6
7
  */
7
8
  import { getAllEntries, getBySlug, getByCategory } from "./db.js";
8
9
  import { rankEntries, computeConfidence, confidenceLabel } from "./scorer.js";
10
+ import { buildRelevanceModel } from "./relevance.js";
9
11
  import { expandQuery, detectCategory, detectPhrases } from "./synonyms.js";
10
12
  import { getTfIdfEngine } from "./tfidf.js";
11
13
  import { createLogger } from "../utils/logger.js";
12
14
  const logger = createLogger("search");
13
15
  /**
14
- * Find and rank APIs for a given task description.
15
- *
16
- * Pipeline:
17
- * 1. Detect phrases (preferFree, open source, etc.) — merge with constraints
18
- * 2. Expand query with synonyms
19
- * 3. SQLite text search on the expanded query (broader candidate pool)
20
- * 4. Also text-search on original query (catches exact matches)
21
- * 5. Detect category for scoring boost
22
- * 6. Rank all candidates with scorer using expanded query + context
16
+ * Relevance gate. A candidate is kept only if:
17
+ * - relevance ≥ minAbsolute, and
18
+ * - relevance ≥ minRelativeToBest × the best candidate's relevance, and
19
+ * - it is not in a clearly unrelated category (categoryFit at its floor while
20
+ * the category was detected with ≥ strongCategoryConfidence) unless its own
21
+ * relevance is ≥ categoryOverride.
23
22
  */
24
- export function findApis(query, constraints, topN = 3) {
25
- logger.debug("findApis called", { query, constraints, topN });
23
+ export const RELEVANCE_GATE = {
24
+ minAbsolute: 0.2,
25
+ minRelativeToBest: 0.6,
26
+ strongCategoryConfidence: 0.5,
27
+ categoryOverride: 0.6,
28
+ };
29
+ /** Above this many entries, use the TF-IDF index to pre-select candidates */
30
+ const SCORE_ALL_ENTRIES_UP_TO = 500;
31
+ /**
32
+ * Find and rank APIs for a task, with stats. Pipeline:
33
+ * 1. Phrase detection (e.g. "free" → preferFree) merged with constraints
34
+ * 2. Category detection
35
+ * 3. Candidate pool: every entry (small KB) or TF-IDF + detected category
36
+ * 4. Score: relevance × bounded quality/DX/pricing/recency prior
37
+ * 5. Gate: drop candidates that are not relevant enough (RELEVANCE_GATE)
38
+ */
39
+ export function findApisDetailed(query, constraints, topN = 3) {
40
+ const empty = { results: [], relevant: [], evaluated: 0, detectedCategory: null };
26
41
  if (!query || query.trim().length === 0) {
27
42
  logger.warn("Empty query provided to findApis");
28
- return [];
43
+ return empty;
29
44
  }
30
45
  const trimmedQuery = query.trim();
31
- // Step 1: Phrase detection — auto-apply preferFree from query language
32
46
  const phrases = detectPhrases(trimmedQuery);
33
47
  const effectiveConstraints = {
34
48
  ...constraints,
35
49
  preferFree: constraints?.preferFree ?? phrases.preferFree,
36
50
  };
37
- // Step 2: Synonym expansion
38
- const expandedQuery = expandQuery(trimmedQuery);
39
- logger.debug("Query expanded", {
40
- original: trimmedQuery,
41
- expanded: expandedQuery.length > trimmedQuery.length
42
- ? `${expandedQuery.slice(0, 80)}...`
43
- : "unchanged",
44
- });
45
- // Step 3: Category detection for scoring context
46
51
  const { category: detectedCategory, confidence: detectedConfidence } = detectCategory(trimmedQuery);
47
- logger.debug("Category detected", { detectedCategory, detectedConfidence });
48
- const scoringContext = {
49
- expandedQuery,
50
- detectedCategory,
51
- detectedConfidence,
52
- };
53
- // Step 4: Build candidate pool via TF-IDF index
54
- // Lazily build the index from all entries on first call
55
52
  const all = getAllEntries();
56
- const engine = getTfIdfEngine();
57
- if (!engine.isBuilt) {
58
- engine.build(all);
59
- logger.debug("TF-IDF index built", { entryCount: engine.size });
60
- }
61
- // TF-IDF search on expanded query (broader recall than LIKE)
62
- const tfIdfCandidates = engine.search(expandedQuery, topN * 8);
63
- let candidates = tfIdfCandidates;
64
- const seenSlugs = new Set(candidates.map((e) => e.slug));
65
- // Also include all entries in the detected category (high-signal candidates)
66
- if (detectedCategory && detectedConfidence >= 0.4) {
67
- const categoryEntries = getByCategory(detectedCategory);
68
- for (const entry of categoryEntries) {
69
- if (!seenSlugs.has(entry.slug)) {
70
- candidates.push(entry);
71
- seenSlugs.add(entry.slug);
53
+ let pool = all;
54
+ if (all.length > SCORE_ALL_ENTRIES_UP_TO) {
55
+ const engine = getTfIdfEngine();
56
+ if (!engine.isBuilt)
57
+ engine.build(all);
58
+ pool = engine.search(expandQuery(trimmedQuery), 100);
59
+ const seen = new Set(pool.map((e) => e.slug));
60
+ if (detectedCategory) {
61
+ for (const e of getByCategory(detectedCategory)) {
62
+ if (!seen.has(e.slug))
63
+ pool.push(e);
72
64
  }
73
65
  }
74
66
  }
75
- // Safety net: if still fewer candidates than requested, add remaining entries
76
- if (candidates.length < topN) {
77
- logger.debug("Broadening to all entries — too few TF-IDF candidates", {
78
- candidateCount: candidates.length,
79
- });
80
- for (const entry of all) {
81
- if (!seenSlugs.has(entry.slug)) {
82
- candidates.push(entry);
83
- seenSlugs.add(entry.slug);
84
- }
85
- }
86
- }
87
- // Step 5: Rank with scorer using expanded query + context
88
- const results = rankEntries(candidates, trimmedQuery, effectiveConstraints, topN, undefined, scoringContext);
89
- // Step 6: Attach confidence to top result
90
- if (results.length > 0 && results[0]) {
91
- const conf = computeConfidence(results[0], detectedCategory, detectedConfidence, results);
67
+ if (pool.length === 0)
68
+ return { ...empty, detectedCategory };
69
+ const context = {
70
+ detectedCategory,
71
+ detectedConfidence,
72
+ relevanceModel: buildRelevanceModel(trimmedQuery, pool),
73
+ };
74
+ const scored = rankEntries(pool, trimmedQuery, effectiveConstraints, pool.length, undefined, context);
75
+ const bestRelevance = Math.max(0, ...scored.map((r) => r.relevance ?? 0));
76
+ const strongCategory = detectedCategory !== null && detectedConfidence >= RELEVANCE_GATE.strongCategoryConfidence;
77
+ const relevant = scored.filter((r) => {
78
+ const rel = r.relevance ?? 0;
79
+ if (rel < RELEVANCE_GATE.minAbsolute)
80
+ return false;
81
+ if (rel < RELEVANCE_GATE.minRelativeToBest * bestRelevance)
82
+ return false;
83
+ const unrelatedCategory = (r.scoreBreakdown["categoryFit"] ?? 1) <= 0.7;
84
+ if (strongCategory && unrelatedCategory && rel < RELEVANCE_GATE.categoryOverride)
85
+ return false;
86
+ return true;
87
+ });
88
+ const results = relevant.slice(0, topN);
89
+ if (results[0]) {
90
+ const conf = computeConfidence(results[0], detectedCategory, detectedConfidence, relevant);
92
91
  results[0].confidence = conf;
93
92
  results[0].confidenceLabel = confidenceLabel(conf);
94
93
  }
95
94
  logger.debug("findApis results", {
96
- candidateCount: candidates.length,
97
- resultCount: results.length,
98
- topSlug: results[0]?.entry.slug,
99
- confidence: results[0]?.confidence,
95
+ query: trimmedQuery,
96
+ evaluated: pool.length,
97
+ relevant: relevant.length,
98
+ top: results.map((r) => `${r.entry.slug}:${r.score.toFixed(2)}`),
100
99
  detectedCategory,
101
100
  });
102
- return results;
101
+ return { results, relevant, evaluated: pool.length, detectedCategory };
102
+ }
103
+ /** Find and rank APIs for a task — relevant results only, best first. */
104
+ export function findApis(query, constraints, topN = 3) {
105
+ return findApisDetailed(query, constraints, topN).results;
103
106
  }
104
107
  /** Get a single API entry by its slug */
105
108
  export function getApiBySlug(slug) {
@@ -14,6 +14,15 @@ export declare const SYNONYM_GROUPS: readonly string[][];
14
14
  * login signin signup register sso oauth identity..."
15
15
  */
16
16
  export declare function expandQuery(query: string): string;
17
+ /**
18
+ * Which synonym groups a query triggers, and which term in the query
19
+ * triggered each one (same matching rules as expandQuery).
20
+ * Used by the relevance model to attach synonyms to the query word they expand.
21
+ */
22
+ export declare function matchSynonymGroups(query: string): Array<{
23
+ term: string;
24
+ group: readonly string[];
25
+ }>;
17
26
  export interface PhraseModifiers {
18
27
  /** User wants free or cheap options */
19
28
  preferFree: boolean;
@@ -229,6 +229,27 @@ export function expandQuery(query) {
229
229
  return query;
230
230
  return `${query} ${[...addedTerms].join(" ")}`;
231
231
  }
232
+ /**
233
+ * Which synonym groups a query triggers, and which term in the query
234
+ * triggered each one (same matching rules as expandQuery).
235
+ * Used by the relevance model to attach synonyms to the query word they expand.
236
+ */
237
+ export function matchSynonymGroups(query) {
238
+ if (!query)
239
+ return [];
240
+ const lowerQuery = query.toLowerCase();
241
+ const matches = [];
242
+ const seen = new Set();
243
+ for (const [term, group] of SORTED_TERM_ENTRIES) {
244
+ if (seen.has(group))
245
+ continue;
246
+ if (buildTermRegex(term).test(lowerQuery)) {
247
+ seen.add(group);
248
+ matches.push({ term, group });
249
+ }
250
+ }
251
+ return matches;
252
+ }
232
253
  const FREE_PATTERNS = [
233
254
  /\bfree\b/i, /\bcheap/i, /\bno cost\b/i, /\bbudget\b/i, /\baffordable\b/i,
234
255
  ];
@@ -284,8 +305,8 @@ const CATEGORY_DETECTORS = [
284
305
  {
285
306
  category: "email",
286
307
  patterns: [
287
- /\bemail\b/i, /\bmail\b/i, /\bsmtp\b/i, /\bnewsletter\b/i,
288
- /\binbox\b/i, /\btransactional\b/i, /\bsend.*mail\b/i,
308
+ /\bemails?\b/i, /\bmails?\b/i, /\bsmtp\b/i, /\bnewsletters?\b/i,
309
+ /\binbox\b/i, /\btransactional\b/i, /\bsend.*mails?\b/i,
289
310
  ],
290
311
  weight: 0.4,
291
312
  },
package/dist/server.d.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  * MCP server setup — creates the server, registers all tools, and handles protocol.
3
3
  * Uses @modelcontextprotocol/sdk McpServer (high-level API).
4
4
  *
5
- * Exports: createServer
5
+ * Exports: createServer, startServer, SERVER_VERSION
6
6
  */
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ /** Server version reported to MCP clients — always package.json's version */
9
+ export declare const SERVER_VERSION: string;
8
10
  export declare function createServer(): McpServer;
9
11
  export declare function startServer(): Promise<void>;
package/dist/server.js CHANGED
@@ -2,8 +2,9 @@
2
2
  * MCP server setup — creates the server, registers all tools, and handles protocol.
3
3
  * Uses @modelcontextprotocol/sdk McpServer (high-level API).
4
4
  *
5
- * Exports: createServer
5
+ * Exports: createServer, startServer, SERVER_VERSION
6
6
  */
7
+ import { createRequire } from "module";
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
10
  import { registerFindBestApi } from "./tools/find-api.js";
@@ -12,10 +13,15 @@ import { registerGetSetupGuide } from "./tools/get-setup-guide.js";
12
13
  import { registerCheckApiFreshness } from "./tools/check-freshness.js";
13
14
  import { createLogger } from "./utils/logger.js";
14
15
  const logger = createLogger("server");
16
+ // package.json sits one level above both src/server.ts and dist/server.js
17
+ const require = createRequire(import.meta.url);
18
+ const pkg = require("../package.json");
19
+ /** Server version reported to MCP clients — always package.json's version */
20
+ export const SERVER_VERSION = pkg.version;
15
21
  export function createServer() {
16
22
  const server = new McpServer({
17
23
  name: "api-oracle",
18
- version: "0.1.0",
24
+ version: SERVER_VERSION,
19
25
  }, {
20
26
  capabilities: {
21
27
  tools: {},
@@ -37,7 +37,7 @@ export function registerCheckApiFreshness(server) {
37
37
  content: [
38
38
  {
39
39
  type: "text",
40
- text: "No entries found in the database. Run `npm run seed` first.",
40
+ text: "The knowledge base is empty: no bundled entries were loaded at startup. Check the server's stderr for 'Skipped invalid entry' or 'Knowledge base is empty' messages.",
41
41
  },
42
42
  ],
43
43
  };
@@ -3,7 +3,24 @@
3
3
  * Finds and recommends the best API for a given task description.
4
4
  * Returns a structured recommendation with quick-start code.
5
5
  *
6
- * Exports: registerFindBestApi
6
+ * Exports: registerFindBestApi, buildFindBestApiResponse, MAX_CANDIDATES
7
7
  */
8
8
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { type SearchConstraints } from "../knowledge/search.js";
10
+ /** Max entries in the `candidates` field */
11
+ export declare const MAX_CANDIDATES = 5;
12
+ /**
13
+ * Build the find_best_api response body.
14
+ *
15
+ * Shape (backward compatible with v0.5.0, plus `candidates`):
16
+ * recommendation — top result with quick-start code
17
+ * alternatives — results 2..3
18
+ * candidates — up to 5 relevant entries with key facts and their
19
+ * relevance, so the calling model can make the final call
20
+ * totalEvaluated — number of knowledge-base entries scored for this query
21
+ * Every entry carries lastVerified; the recommendation also carries
22
+ * freshness {lastVerified, daysSinceVerified, level} and a freshnessNote
23
+ * when it is not fresh.
24
+ */
25
+ export declare function buildFindBestApiResponse(task: string, constraints?: SearchConstraints): Record<string, unknown>;
9
26
  export declare function registerFindBestApi(server: McpServer): void;
@@ -3,10 +3,11 @@
3
3
  * Finds and recommends the best API for a given task description.
4
4
  * Returns a structured recommendation with quick-start code.
5
5
  *
6
- * Exports: registerFindBestApi
6
+ * Exports: registerFindBestApi, buildFindBestApiResponse, MAX_CANDIDATES
7
7
  */
8
8
  import { z } from "zod";
9
- import { findApis } from "../knowledge/search.js";
9
+ import { findApisDetailed } from "../knowledge/search.js";
10
+ import { checkStaleness } from "../updater/staleness.js";
10
11
  import { createLogger } from "../utils/logger.js";
11
12
  const logger = createLogger("tool:find-api");
12
13
  const inputSchema = {
@@ -29,70 +30,115 @@ const inputSchema = {
29
30
  .optional()
30
31
  .describe("Optional constraints to filter recommendations"),
31
32
  };
33
+ /** Max entries in the `candidates` field */
34
+ export const MAX_CANDIDATES = 5;
35
+ function pricingSummary(entry) {
36
+ return ([
37
+ entry.pricing.freeTier ? `Free: ${entry.pricing.freeTier}` : null,
38
+ entry.pricing.startingPrice ? `Paid from ${entry.pricing.startingPrice}` : null,
39
+ entry.pricing.model === "usage_based" ? "Usage-based (no monthly fee)" : null,
40
+ ]
41
+ .filter(Boolean)
42
+ .join(" | ") || entry.pricing.model);
43
+ }
44
+ /**
45
+ * lastVerified plus how stale that is. Dates are never changed automatically:
46
+ * an entry is only "fresh" if a human re-verified it within 90 days.
47
+ */
48
+ function freshness(entry) {
49
+ const s = checkStaleness(entry);
50
+ return { lastVerified: s.lastVerified, daysSinceVerified: s.daysSinceVerified, level: s.level };
51
+ }
52
+ function freshnessNote(f) {
53
+ return (`This entry was last verified on ${f.lastVerified} (${f.daysSinceVerified} days ago). ` +
54
+ "Pricing, rate limits and SDK details may have changed since — confirm them on the provider's pricing and docs pages.");
55
+ }
56
+ /**
57
+ * Build the find_best_api response body.
58
+ *
59
+ * Shape (backward compatible with v0.5.0, plus `candidates`):
60
+ * recommendation — top result with quick-start code
61
+ * alternatives — results 2..3
62
+ * candidates — up to 5 relevant entries with key facts and their
63
+ * relevance, so the calling model can make the final call
64
+ * totalEvaluated — number of knowledge-base entries scored for this query
65
+ * Every entry carries lastVerified; the recommendation also carries
66
+ * freshness {lastVerified, daysSinceVerified, level} and a freshnessNote
67
+ * when it is not fresh.
68
+ */
69
+ export function buildFindBestApiResponse(task, constraints) {
70
+ const outcome = findApisDetailed(task, constraints, 3);
71
+ const results = outcome.results;
72
+ if (results.length === 0) {
73
+ return {
74
+ error: "No matching APIs found",
75
+ suggestion: "Try a broader task description, or the API category may not be in the database yet.",
76
+ totalEvaluated: outcome.evaluated,
77
+ candidates: [],
78
+ };
79
+ }
80
+ const [top, ...alternatives] = results;
81
+ const entry = top.entry;
82
+ const bestExample = entry.codeExamples[0];
83
+ const topFreshness = freshness(entry);
84
+ return {
85
+ recommendation: {
86
+ name: entry.name,
87
+ slug: entry.slug,
88
+ why: `${entry.bestFor}. ${entry.qualityJustification}`,
89
+ qualityScore: entry.qualityScore,
90
+ confidence: top.confidence ?? null,
91
+ confidenceLabel: top.confidenceLabel ?? null,
92
+ scoreBreakdown: top.scoreBreakdown,
93
+ quickStart: {
94
+ install: entry.sdk.installCommand,
95
+ envVar: `${entry.auth.envVarName}=your_key_here`,
96
+ authSnippet: entry.auth.codeSnippet,
97
+ codeExample: bestExample?.code ?? "See documentation",
98
+ codeExampleTitle: bestExample?.title ?? "",
99
+ notes: bestExample?.notes ?? "",
100
+ },
101
+ gotchas: entry.gotchas,
102
+ pricing: pricingSummary(entry),
103
+ website: entry.website,
104
+ rateLimitNote: `${entry.rateLimits.tier}: ${entry.rateLimits.limit}`,
105
+ lastVerified: entry.lastVerified,
106
+ freshness: topFreshness,
107
+ ...(topFreshness.level === "fresh" ? {} : { freshnessNote: freshnessNote(topFreshness) }),
108
+ },
109
+ alternatives: alternatives.map((r) => ({
110
+ name: r.entry.name,
111
+ slug: r.entry.slug,
112
+ why: r.entry.bestFor,
113
+ qualityScore: r.entry.qualityScore,
114
+ score: Math.round(r.score * 100) / 100,
115
+ lastVerified: r.entry.lastVerified,
116
+ })),
117
+ candidates: outcome.relevant.slice(0, MAX_CANDIDATES).map((r) => ({
118
+ slug: r.entry.slug,
119
+ name: r.entry.name,
120
+ category: r.entry.category,
121
+ bestFor: r.entry.bestFor,
122
+ relevance: Math.round((r.relevance ?? 0) * 100) / 100,
123
+ score: Math.round(r.score * 100) / 100,
124
+ qualityScore: r.entry.qualityScore,
125
+ pricing: pricingSummary(r.entry),
126
+ pricingModel: r.entry.pricing.model,
127
+ primaryLanguage: r.entry.sdk.primaryLanguage,
128
+ authMethod: r.entry.auth.method,
129
+ topGotcha: r.entry.gotchas[0] ?? null,
130
+ website: r.entry.website,
131
+ lastVerified: r.entry.lastVerified,
132
+ freshnessLevel: freshness(r.entry).level,
133
+ })),
134
+ totalEvaluated: outcome.evaluated,
135
+ };
136
+ }
32
137
  export function registerFindBestApi(server) {
33
- server.tool("find_best_api", "Find and recommend the best API for a programming task. Returns top recommendation with quick-start code, gotchas, and alternatives.", inputSchema, async ({ task, constraints }) => {
138
+ server.tool("find_best_api", "Find and recommend the best API for a programming task. Returns a top recommendation with quick-start code, gotchas and alternatives, plus `candidates`: up to 5 relevant APIs with key facts and relevance scores so you can make the final choice for the user's context.", inputSchema, async ({ task, constraints }) => {
34
139
  logger.info("find_best_api called", { task, constraints });
35
140
  try {
36
- const results = findApis(task, constraints, 3);
37
- if (results.length === 0) {
38
- return {
39
- content: [
40
- {
41
- type: "text",
42
- text: JSON.stringify({
43
- error: "No matching APIs found",
44
- suggestion: "Try a broader task description, or the API category may not be in the database yet.",
45
- totalEvaluated: 0,
46
- }),
47
- },
48
- ],
49
- };
50
- }
51
- const [top, ...alternatives] = results;
52
- const entry = top.entry;
53
- // Build a one-line pricing summary
54
- const pricingSummary = [
55
- entry.pricing.freeTier ? `Free: ${entry.pricing.freeTier}` : null,
56
- entry.pricing.startingPrice
57
- ? `Paid from ${entry.pricing.startingPrice}`
58
- : null,
59
- entry.pricing.model === "usage_based" ? "Usage-based (no monthly fee)" : null,
60
- ]
61
- .filter(Boolean)
62
- .join(" | ") || entry.pricing.model;
63
- // Select best code example — prefer the first one
64
- const bestExample = entry.codeExamples[0];
65
- const response = {
66
- recommendation: {
67
- name: entry.name,
68
- slug: entry.slug,
69
- why: `${entry.bestFor}. ${entry.qualityJustification}`,
70
- qualityScore: entry.qualityScore,
71
- confidence: top.confidence ?? null,
72
- confidenceLabel: top.confidenceLabel ?? null,
73
- scoreBreakdown: top.scoreBreakdown,
74
- quickStart: {
75
- install: entry.sdk.installCommand,
76
- envVar: `${entry.auth.envVarName}=your_key_here`,
77
- authSnippet: entry.auth.codeSnippet,
78
- codeExample: bestExample?.code ?? "See documentation",
79
- codeExampleTitle: bestExample?.title ?? "",
80
- notes: bestExample?.notes ?? "",
81
- },
82
- gotchas: entry.gotchas,
83
- pricing: pricingSummary,
84
- website: entry.website,
85
- rateLimitNote: `${entry.rateLimits.tier}: ${entry.rateLimits.limit}`,
86
- },
87
- alternatives: alternatives.map((r) => ({
88
- name: r.entry.name,
89
- slug: r.entry.slug,
90
- why: r.entry.bestFor,
91
- qualityScore: r.entry.qualityScore,
92
- score: Math.round(r.score * 100) / 100,
93
- })),
94
- totalEvaluated: results.length + alternatives.length,
95
- };
141
+ const response = buildFindBestApiResponse(task, constraints);
96
142
  return {
97
143
  content: [
98
144
  {
@@ -4,6 +4,7 @@
4
4
  * Exports: config (singleton config object)
5
5
  */
6
6
  export interface Config {
7
+ /** ":memory:" unless DB_PATH is set explicitly */
7
8
  dbPath: string;
8
9
  logLevel: string;
9
10
  nodeEnv: string;
@@ -4,8 +4,11 @@
4
4
  * Exports: config (singleton config object)
5
5
  */
6
6
  function loadConfig() {
7
+ const explicitDbPath = process.env["DB_PATH"]?.trim();
7
8
  return {
8
- dbPath: process.env["DB_PATH"] ?? "./data/api-oracle.db",
9
+ // Default: in-memory DB loaded from the bundled entries at startup, so
10
+ // nothing is written to the user's working directory.
11
+ dbPath: explicitDbPath ? explicitDbPath : ":memory:",
9
12
  logLevel: process.env["LOG_LEVEL"] ?? "info",
10
13
  nodeEnv: process.env["NODE_ENV"] ?? "development",
11
14
  };
@@ -2,8 +2,15 @@
2
2
  * Simple structured logger — no external dependencies.
3
3
  * Writes to stderr so MCP stdout stays clean for protocol messages.
4
4
  *
5
- * Exports: logger (default instance), createLogger
5
+ * Exports: logger (default instance), createLogger, currentLogLevel
6
+ * Respects LOG_LEVEL (default: info).
6
7
  */
8
+ type LogLevel = "debug" | "info" | "warn" | "error";
9
+ /**
10
+ * Minimum level to emit, from LOG_LEVEL (debug | info | warn | error).
11
+ * Read on each call so it can be changed at runtime/tests. Default: info.
12
+ */
13
+ export declare function currentLogLevel(): LogLevel;
7
14
  declare function createLogger(namespace: string): {
8
15
  debug: (message: string, data?: unknown) => void;
9
16
  info: (message: string, data?: unknown) => void;
@@ -2,7 +2,8 @@
2
2
  * Simple structured logger — no external dependencies.
3
3
  * Writes to stderr so MCP stdout stays clean for protocol messages.
4
4
  *
5
- * Exports: logger (default instance), createLogger
5
+ * Exports: logger (default instance), createLogger, currentLogLevel
6
+ * Respects LOG_LEVEL (default: info).
6
7
  */
7
8
  function formatEntry(entry) {
8
9
  const base = `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}`;
@@ -11,8 +12,19 @@ function formatEntry(entry) {
11
12
  }
12
13
  return base;
13
14
  }
15
+ const LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
16
+ /**
17
+ * Minimum level to emit, from LOG_LEVEL (debug | info | warn | error).
18
+ * Read on each call so it can be changed at runtime/tests. Default: info.
19
+ */
20
+ export function currentLogLevel() {
21
+ const raw = (process.env["LOG_LEVEL"] ?? "info").trim().toLowerCase();
22
+ return raw in LEVEL_ORDER ? raw : "info";
23
+ }
14
24
  function createLogger(namespace) {
15
25
  function log(level, message, data) {
26
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[currentLogLevel()])
27
+ return;
16
28
  const entry = {
17
29
  level,
18
30
  message: namespace ? `[${namespace}] ${message}` : message,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adityanair98/api-oracle",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server that finds, evaluates, and recommends the best API for any programming task",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "license": "MIT",
27
27
  "scripts": {
28
- "build": "tsc && cp -r src/dashboard/public dist/dashboard/public",
28
+ "build": "rm -rf dist && tsc && cp -r src/dashboard/public dist/dashboard/public",
29
29
  "dev": "tsx src/index.ts",
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
@@ -33,11 +33,12 @@
33
33
  "seed": "tsx scripts/seed-db.ts",
34
34
  "e2e": "tsx scripts/e2e-test.ts",
35
35
  "analyze": "tsx scripts/scoring-analysis.ts",
36
+ "eval": "tsx scripts/eval.ts",
36
37
  "refresh-report": "tsx scripts/refresh-report.ts",
37
38
  "lint-entries": "tsx scripts/lint-entries.ts",
38
39
  "dashboard": "tsx src/dashboard/server.ts",
39
40
  "dashboard:dev": "tsx watch src/dashboard/server.ts",
40
- "prepublishOnly": "npm run build && npm test"
41
+ "prepublishOnly": "node -e \"if(/FILL_ME_/.test(require('fs').readFileSync('README.md','utf8'))){console.error('README.md still has FILL_ME_ placeholders');process.exit(1)}\" && npm run build && npm test"
41
42
  },
42
43
  "dependencies": {
43
44
  "@modelcontextprotocol/sdk": "1.27.1",