@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.
- package/README.md +78 -26
- package/dist/cli.js +24 -11
- package/dist/dashboard/routes/api.js +1 -1
- package/dist/dashboard/server.d.ts +7 -1
- package/dist/dashboard/server.js +48 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -5
- package/dist/knowledge/db.d.ts +10 -0
- package/dist/knowledge/db.js +38 -2
- package/dist/knowledge/loader.d.ts +56 -0
- package/dist/knowledge/loader.js +116 -0
- package/dist/knowledge/relevance.d.ts +66 -0
- package/dist/knowledge/relevance.js +267 -0
- package/dist/knowledge/scorer.d.ts +41 -15
- package/dist/knowledge/scorer.js +151 -156
- package/dist/knowledge/search.d.ts +37 -12
- package/dist/knowledge/search.js +74 -71
- package/dist/knowledge/synonyms.d.ts +9 -0
- package/dist/knowledge/synonyms.js +23 -2
- package/dist/server.d.ts +3 -1
- package/dist/server.js +8 -2
- package/dist/tools/check-freshness.js +1 -1
- package/dist/tools/find-api.d.ts +18 -1
- package/dist/tools/find-api.js +109 -63
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.js +4 -1
- package/dist/utils/logger.d.ts +8 -1
- package/dist/utils/logger.js +13 -1
- package/package.json +4 -3
- package/dist/dashboard/public/public/app.js +0 -1004
- package/dist/dashboard/public/public/index.html +0 -142
- package/dist/dashboard/public/public/styles.css +0 -1464
package/dist/knowledge/search.js
CHANGED
|
@@ -1,105 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Search layer —
|
|
3
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
57
|
-
if (
|
|
58
|
-
engine
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
/\
|
|
288
|
-
/\binbox\b/i, /\btransactional\b/i, /\bsend.*
|
|
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:
|
|
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: "
|
|
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
|
};
|
package/dist/tools/find-api.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/find-api.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
{
|
package/dist/utils/config.d.ts
CHANGED
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/logger.js
CHANGED
|
@@ -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.
|
|
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",
|