@adityanair98/api-oracle 0.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +74 -0
  5. package/dist/dashboard/public/app.js +1004 -0
  6. package/dist/dashboard/public/index.html +142 -0
  7. package/dist/dashboard/public/public/app.js +1004 -0
  8. package/dist/dashboard/public/public/index.html +142 -0
  9. package/dist/dashboard/public/public/styles.css +1464 -0
  10. package/dist/dashboard/public/styles.css +1464 -0
  11. package/dist/dashboard/routes/api.d.ts +7 -0
  12. package/dist/dashboard/routes/api.js +245 -0
  13. package/dist/dashboard/server.d.ts +9 -0
  14. package/dist/dashboard/server.js +45 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +23 -0
  17. package/dist/knowledge/db.d.ts +22 -0
  18. package/dist/knowledge/db.js +182 -0
  19. package/dist/knowledge/schema.d.ts +275 -0
  20. package/dist/knowledge/schema.js +135 -0
  21. package/dist/knowledge/scorer.d.ts +63 -0
  22. package/dist/knowledge/scorer.js +314 -0
  23. package/dist/knowledge/search.d.ts +37 -0
  24. package/dist/knowledge/search.js +111 -0
  25. package/dist/knowledge/synonyms.d.ts +36 -0
  26. package/dist/knowledge/synonyms.js +523 -0
  27. package/dist/knowledge/tfidf.d.ts +42 -0
  28. package/dist/knowledge/tfidf.js +138 -0
  29. package/dist/server.d.ts +9 -0
  30. package/dist/server.js +40 -0
  31. package/dist/tools/check-freshness.d.ts +9 -0
  32. package/dist/tools/check-freshness.js +95 -0
  33. package/dist/tools/compare-apis.d.ts +8 -0
  34. package/dist/tools/compare-apis.js +149 -0
  35. package/dist/tools/find-api.d.ts +9 -0
  36. package/dist/tools/find-api.js +120 -0
  37. package/dist/tools/get-setup-guide.d.ts +8 -0
  38. package/dist/tools/get-setup-guide.js +127 -0
  39. package/dist/updater/linter.d.ts +31 -0
  40. package/dist/updater/linter.js +219 -0
  41. package/dist/updater/report.d.ts +29 -0
  42. package/dist/updater/report.js +96 -0
  43. package/dist/updater/staleness.d.ts +39 -0
  44. package/dist/updater/staleness.js +66 -0
  45. package/dist/updater/version-tracker.d.ts +28 -0
  46. package/dist/updater/version-tracker.js +50 -0
  47. package/dist/utils/config.d.ts +11 -0
  48. package/dist/utils/config.js +13 -0
  49. package/dist/utils/logger.d.ts +20 -0
  50. package/dist/utils/logger.js +32 -0
  51. package/package.json +56 -0
  52. package/src/entries/ai/anthropic.json +95 -0
  53. package/src/entries/ai/eleven-labs.json +90 -0
  54. package/src/entries/ai/openai.json +95 -0
  55. package/src/entries/ai/replicate.json +87 -0
  56. package/src/entries/ai/resemble-ai.json +88 -0
  57. package/src/entries/ai/stability-ai.json +89 -0
  58. package/src/entries/analytics/posthog.json +88 -0
  59. package/src/entries/analytics/sentry.json +84 -0
  60. package/src/entries/auth/auth0.json +90 -0
  61. package/src/entries/auth/clerk.json +95 -0
  62. package/src/entries/cms/contentful.json +92 -0
  63. package/src/entries/cms/sanity.json +92 -0
  64. package/src/entries/cms/strapi.json +93 -0
  65. package/src/entries/commerce/medusa.json +91 -0
  66. package/src/entries/commerce/shopify-api.json +91 -0
  67. package/src/entries/communication/sendbird.json +85 -0
  68. package/src/entries/communication/stream-chat.json +94 -0
  69. package/src/entries/database/firebase.json +88 -0
  70. package/src/entries/database/neon.json +94 -0
  71. package/src/entries/database/planetscale.json +95 -0
  72. package/src/entries/database/supabase.json +94 -0
  73. package/src/entries/database/upstash.json +94 -0
  74. package/src/entries/devops/fly-io.json +90 -0
  75. package/src/entries/devops/netlify.json +90 -0
  76. package/src/entries/devops/railway.json +90 -0
  77. package/src/entries/devops/vercel.json +90 -0
  78. package/src/entries/email/mailgun.json +91 -0
  79. package/src/entries/email/postmark.json +91 -0
  80. package/src/entries/email/resend.json +89 -0
  81. package/src/entries/email/sendgrid.json +90 -0
  82. package/src/entries/forms/formspark.json +85 -0
  83. package/src/entries/forms/typeform.json +98 -0
  84. package/src/entries/infrastructure/aws-s3.json +104 -0
  85. package/src/entries/infrastructure/cloudflare-r2.json +92 -0
  86. package/src/entries/infrastructure/cloudflare-workers.json +92 -0
  87. package/src/entries/infrastructure/digital-ocean-spaces.json +87 -0
  88. package/src/entries/integration/nango.json +90 -0
  89. package/src/entries/integration/zapier.json +92 -0
  90. package/src/entries/maps/google-maps.json +89 -0
  91. package/src/entries/maps/mapbox.json +87 -0
  92. package/src/entries/media/deepgram.json +84 -0
  93. package/src/entries/media/imgix.json +84 -0
  94. package/src/entries/media/mux.json +94 -0
  95. package/src/entries/messaging/ably.json +94 -0
  96. package/src/entries/messaging/pusher.json +94 -0
  97. package/src/entries/messaging/twilio.json +94 -0
  98. package/src/entries/messaging/vonage.json +89 -0
  99. package/src/entries/notifications/knock.json +84 -0
  100. package/src/entries/notifications/novu.json +84 -0
  101. package/src/entries/notifications/onesignal.json +84 -0
  102. package/src/entries/payments/lemonsqueezy.json +91 -0
  103. package/src/entries/payments/paddle.json +90 -0
  104. package/src/entries/payments/paypal.json +91 -0
  105. package/src/entries/payments/razorpay.json +85 -0
  106. package/src/entries/payments/square.json +91 -0
  107. package/src/entries/payments/stripe.json +96 -0
  108. package/src/entries/scheduling/cal-com.json +90 -0
  109. package/src/entries/scheduling/calendly.json +90 -0
  110. package/src/entries/search/algolia.json +96 -0
  111. package/src/entries/security/arcjet.json +89 -0
  112. package/src/entries/security/snyk.json +90 -0
  113. package/src/entries/storage/cloudinary.json +93 -0
  114. package/src/entries/storage/uploadthing.json +90 -0
  115. package/src/entries/testing/browserstack.json +86 -0
  116. package/src/entries/testing/checkly.json +89 -0
  117. package/src/entries/workflow/inngest.json +88 -0
  118. package/src/entries/workflow/temporal.json +90 -0
  119. package/src/entries/workflow/trigger-dev.json +89 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * TF-IDF text search engine for API entries.
3
+ *
4
+ * Builds an in-memory index from entries and provides relevance-ranked search
5
+ * without requiring any external API calls or embedding models. Uses weighted
6
+ * multi-field document construction to boost precision:
7
+ * name (x4) > bestFor/subcategory (x3) > useCases (x2) > description (x1)
8
+ *
9
+ * TF-IDF formula:
10
+ * score(doc, query) = sum_t( TF(t, doc) * IDF(t) ) for each query term t
11
+ * IDF(t) = log((N + 1) / (df(t) + 1)) + 1 (Laplace smoothing)
12
+ *
13
+ * Exports: TfIdfEngine, getTfIdfEngine, resetTfIdfEngine
14
+ */
15
+ import { tokenize } from "./scorer.js";
16
+ // ─── Field weights for document construction ──────────────────────────────────
17
+ const FIELD_WEIGHTS = {
18
+ name: 4, // API name is the strongest identity signal
19
+ subcategory: 3, // e.g. "transactional-email", "push-notification"
20
+ bestFor: 3, // Concise purpose statement written for discoverability
21
+ useCases: 2, // Task-oriented descriptions — close to real queries
22
+ description: 1, // Full prose description (lower weight — can be noisy)
23
+ category: 1, // Broad category (broad signal only)
24
+ };
25
+ // ─── TF-IDF Engine ────────────────────────────────────────────────────────────
26
+ export class TfIdfEngine {
27
+ vectors = [];
28
+ idf = new Map();
29
+ entryBySlug = new Map();
30
+ _built = false;
31
+ /**
32
+ * Build the TF-IDF index from a list of entries.
33
+ * Can be called again to rebuild with updated entries.
34
+ */
35
+ build(entries) {
36
+ this.vectors = [];
37
+ this.idf = new Map();
38
+ this.entryBySlug = new Map();
39
+ this._built = false;
40
+ const N = entries.length;
41
+ if (N === 0)
42
+ return;
43
+ // Document frequency: how many documents contain each term
44
+ const df = new Map();
45
+ for (const entry of entries) {
46
+ this.entryBySlug.set(entry.slug, entry);
47
+ const termCounts = new Map();
48
+ const addTokens = (text, weight) => {
49
+ for (const token of tokenize(text)) {
50
+ termCounts.set(token, (termCounts.get(token) ?? 0) + weight);
51
+ }
52
+ };
53
+ addTokens(entry.name, FIELD_WEIGHTS.name);
54
+ addTokens(entry.subcategory.replace(/-/g, " "), FIELD_WEIGHTS.subcategory);
55
+ addTokens(entry.bestFor, FIELD_WEIGHTS.bestFor);
56
+ addTokens(entry.category, FIELD_WEIGHTS.category);
57
+ addTokens(entry.description.slice(0, 300), FIELD_WEIGHTS.description);
58
+ for (const uc of entry.useCases) {
59
+ addTokens(uc.task, FIELD_WEIGHTS.useCases);
60
+ }
61
+ // Document length = sum of all weighted counts (for TF normalization)
62
+ let totalWeight = 0;
63
+ for (const count of termCounts.values())
64
+ totalWeight += count;
65
+ this.vectors.push({ slug: entry.slug, termCounts, totalWeight });
66
+ // Update document frequency (count each term once per document)
67
+ for (const term of termCounts.keys()) {
68
+ df.set(term, (df.get(term) ?? 0) + 1);
69
+ }
70
+ }
71
+ // Compute IDF with Laplace smoothing to avoid division-by-zero
72
+ // log((N+1)/(df+1)) + 1 ensures unknown terms get IDF ≈ 1
73
+ for (const [term, count] of df) {
74
+ this.idf.set(term, Math.log((N + 1) / (count + 1)) + 1);
75
+ }
76
+ this._built = true;
77
+ }
78
+ /**
79
+ * Search the index for entries most relevant to the query.
80
+ * Returns up to topN entries sorted by TF-IDF score descending.
81
+ * Entries with zero score are excluded.
82
+ */
83
+ search(query, topN = 10) {
84
+ if (!this._built || this.vectors.length === 0)
85
+ return [];
86
+ const queryTokens = tokenize(query);
87
+ if (queryTokens.size === 0)
88
+ return [];
89
+ const scores = [];
90
+ for (const doc of this.vectors) {
91
+ let score = 0;
92
+ for (const term of queryTokens) {
93
+ const rawCount = doc.termCounts.get(term) ?? 0;
94
+ if (rawCount === 0)
95
+ continue;
96
+ // TF = count / document_length (normalized by total weighted length)
97
+ const tf = rawCount / Math.max(doc.totalWeight, 1);
98
+ // IDF: terms not in any document get default IDF=1 (unknown query term)
99
+ const idf = this.idf.get(term) ?? 1;
100
+ score += tf * idf;
101
+ }
102
+ if (score > 0) {
103
+ scores.push({ slug: doc.slug, score });
104
+ }
105
+ }
106
+ scores.sort((a, b) => b.score - a.score);
107
+ const results = [];
108
+ for (const { slug } of scores.slice(0, topN)) {
109
+ const entry = this.entryBySlug.get(slug);
110
+ if (entry)
111
+ results.push(entry);
112
+ }
113
+ return results;
114
+ }
115
+ get isBuilt() {
116
+ return this._built;
117
+ }
118
+ /** Number of indexed documents */
119
+ get size() {
120
+ return this.vectors.length;
121
+ }
122
+ }
123
+ // ─── Module-level singleton ────────────────────────────────────────────────────
124
+ let _engine = null;
125
+ /** Get the shared engine instance (lazily created) */
126
+ export function getTfIdfEngine() {
127
+ if (!_engine) {
128
+ _engine = new TfIdfEngine();
129
+ }
130
+ return _engine;
131
+ }
132
+ /**
133
+ * Reset the shared engine (forces rebuild on next search).
134
+ * Must be called in test afterEach when the DB is replaced.
135
+ */
136
+ export function resetTfIdfEngine() {
137
+ _engine = null;
138
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MCP server setup — creates the server, registers all tools, and handles protocol.
3
+ * Uses @modelcontextprotocol/sdk McpServer (high-level API).
4
+ *
5
+ * Exports: createServer
6
+ */
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ export declare function createServer(): McpServer;
9
+ export declare function startServer(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MCP server setup — creates the server, registers all tools, and handles protocol.
3
+ * Uses @modelcontextprotocol/sdk McpServer (high-level API).
4
+ *
5
+ * Exports: createServer
6
+ */
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { registerFindBestApi } from "./tools/find-api.js";
10
+ import { registerCompareApis } from "./tools/compare-apis.js";
11
+ import { registerGetSetupGuide } from "./tools/get-setup-guide.js";
12
+ import { registerCheckApiFreshness } from "./tools/check-freshness.js";
13
+ import { createLogger } from "./utils/logger.js";
14
+ const logger = createLogger("server");
15
+ export function createServer() {
16
+ const server = new McpServer({
17
+ name: "api-oracle",
18
+ version: "0.1.0",
19
+ }, {
20
+ capabilities: {
21
+ tools: {},
22
+ },
23
+ instructions: "API Oracle helps developers find, evaluate, and use the best API for any programming task. " +
24
+ "Use find_best_api to get a recommendation, compare_apis to compare specific options, " +
25
+ "and get_api_setup_guide for complete setup instructions.",
26
+ });
27
+ // Register all tools
28
+ registerFindBestApi(server);
29
+ registerCompareApis(server);
30
+ registerGetSetupGuide(server);
31
+ registerCheckApiFreshness(server);
32
+ logger.info("Server created with 4 tools registered");
33
+ return server;
34
+ }
35
+ export async function startServer() {
36
+ const server = createServer();
37
+ const transport = new StdioServerTransport();
38
+ await server.connect(transport);
39
+ logger.info("API Oracle MCP server running on stdio");
40
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MCP Tool: check_api_freshness
3
+ * Reports how up-to-date the API Oracle knowledge base entries are.
4
+ * Returns a staleness summary — useful before making API recommendations.
5
+ *
6
+ * Exports: registerCheckApiFreshness
7
+ */
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ export declare function registerCheckApiFreshness(server: McpServer): void;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * MCP Tool: check_api_freshness
3
+ * Reports how up-to-date the API Oracle knowledge base entries are.
4
+ * Returns a staleness summary — useful before making API recommendations.
5
+ *
6
+ * Exports: registerCheckApiFreshness
7
+ */
8
+ import { z } from "zod";
9
+ import { getAllEntries } from "../knowledge/db.js";
10
+ import { checkAllEntries } from "../updater/staleness.js";
11
+ import { buildFreshnessReport, formatReport, formatBriefSummary } from "../updater/report.js";
12
+ import { createLogger } from "../utils/logger.js";
13
+ const logger = createLogger("tool:check-freshness");
14
+ const inputSchema = {
15
+ slug: z
16
+ .string()
17
+ .optional()
18
+ .describe("Optional: check freshness for a specific API by slug (e.g. 'stripe'). " +
19
+ "If omitted, returns a summary for all entries."),
20
+ level: z
21
+ .enum(["aging", "stale", "critical"])
22
+ .optional()
23
+ .describe("Optional: filter to only show entries at this staleness level or worse. " +
24
+ "Values: 'aging' (3–6 months), 'stale' (6–12 months), 'critical' (>12 months)."),
25
+ brief: z
26
+ .boolean()
27
+ .optional()
28
+ .describe("Optional: return a one-line summary instead of a full report (default: false)."),
29
+ };
30
+ export function registerCheckApiFreshness(server) {
31
+ server.tool("check_api_freshness", "Check how up-to-date the API Oracle knowledge base is. Returns a freshness report showing which entries need review based on their lastVerified date.", inputSchema, async ({ slug, level, brief }) => {
32
+ logger.info("check_api_freshness called", { slug, level, brief });
33
+ try {
34
+ const entries = getAllEntries();
35
+ if (entries.length === 0) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: "No entries found in the database. Run `npm run seed` first.",
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ // Single-entry lookup
46
+ if (slug) {
47
+ const entry = entries.find((e) => e.slug === slug);
48
+ if (!entry) {
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text",
53
+ text: `No entry found with slug: ${slug}`,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ const [info] = checkAllEntries([entry]);
59
+ if (!info) {
60
+ return {
61
+ content: [{ type: "text", text: "Error computing staleness." }],
62
+ };
63
+ }
64
+ const text = `**${entry.name}** (${entry.slug})\n` +
65
+ `Last verified: ${info.lastVerified}\n` +
66
+ `Age: ${info.daysSinceVerified} days\n` +
67
+ `Status: ${info.level}`;
68
+ return { content: [{ type: "text", text }] };
69
+ }
70
+ // Full report
71
+ let allInfos = checkAllEntries(entries);
72
+ if (level) {
73
+ const levelOrder = { fresh: 0, aging: 1, stale: 2, critical: 3 };
74
+ const minLevel = levelOrder[level] ?? 0;
75
+ allInfos = allInfos.filter((i) => (levelOrder[i.level] ?? 0) >= minLevel);
76
+ }
77
+ const report = buildFreshnessReport(allInfos);
78
+ if (brief) {
79
+ return {
80
+ content: [{ type: "text", text: formatBriefSummary(report) }],
81
+ };
82
+ }
83
+ return {
84
+ content: [{ type: "text", text: formatReport(report) }],
85
+ };
86
+ }
87
+ catch (err) {
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ logger.error("check_api_freshness error", { error: message });
90
+ return {
91
+ content: [{ type: "text", text: `Error: ${message}` }],
92
+ };
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * MCP Tool: compare_apis
3
+ * Side-by-side comparison of multiple APIs by their slugs.
4
+ *
5
+ * Exports: registerCompareApis
6
+ */
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ export declare function registerCompareApis(server: McpServer): void;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * MCP Tool: compare_apis
3
+ * Side-by-side comparison of multiple APIs by their slugs.
4
+ *
5
+ * Exports: registerCompareApis
6
+ */
7
+ import { z } from "zod";
8
+ import { getApiBySlug } from "../knowledge/search.js";
9
+ import { createLogger } from "../utils/logger.js";
10
+ const logger = createLogger("tool:compare-apis");
11
+ const inputSchema = {
12
+ slugs: z
13
+ .array(z.string().min(1))
14
+ .min(2)
15
+ .max(5)
16
+ .describe("Slugs of APIs to compare, e.g. ['resend', 'sendgrid', 'postmark']"),
17
+ criteria: z
18
+ .string()
19
+ .optional()
20
+ .describe("Optional focus area for the comparison, e.g. 'pricing', 'deliverability', 'developer experience'"),
21
+ };
22
+ export function registerCompareApis(server) {
23
+ server.tool("compare_apis", "Compare multiple APIs side-by-side. Provide 2-5 API slugs and an optional criteria focus.", inputSchema, async ({ slugs, criteria }) => {
24
+ logger.info("compare_apis called", { slugs, criteria });
25
+ try {
26
+ // Look up all requested APIs
27
+ const entries = slugs.map((slug) => {
28
+ const entry = getApiBySlug(slug);
29
+ return { slug, entry };
30
+ });
31
+ // Report any not found
32
+ const notFound = entries.filter((e) => !e.entry).map((e) => e.slug);
33
+ const found = entries.filter((e) => e.entry !== null);
34
+ if (found.length < 2) {
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: JSON.stringify({
40
+ error: "Not enough valid APIs to compare",
41
+ notFound,
42
+ suggestion: "Use find_best_api to discover valid API slugs.",
43
+ }),
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ // Build comparison entries
49
+ const comparison = found.map(({ entry }) => {
50
+ const pricingSummary = [
51
+ entry.pricing.freeTier ? `Free: ${entry.pricing.freeTier}` : null,
52
+ entry.pricing.startingPrice
53
+ ? `Paid from ${entry.pricing.startingPrice}`
54
+ : null,
55
+ ]
56
+ .filter(Boolean)
57
+ .join(" | ") || entry.pricing.model;
58
+ // Derive strengths from quality justification + best use cases
59
+ const perfectFits = entry.useCases
60
+ .filter((uc) => uc.fit === "perfect")
61
+ .map((uc) => uc.task);
62
+ const partialFits = entry.useCases
63
+ .filter((uc) => uc.fit === "partial")
64
+ .map((uc) => uc.task);
65
+ const strengths = [
66
+ ...perfectFits.slice(0, 2),
67
+ entry.sdk.primaryLanguage === "typescript"
68
+ ? "First-class TypeScript SDK"
69
+ : null,
70
+ entry.reliability.uptimeGuarantee
71
+ ? `${entry.reliability.uptimeGuarantee} uptime guarantee`
72
+ : null,
73
+ ].filter((s) => s !== null);
74
+ const weaknesses = [
75
+ ...partialFits.slice(0, 1).map((t) => `Limited support for: ${t}`),
76
+ ...entry.gotchas.slice(0, 1),
77
+ ].filter(Boolean);
78
+ return {
79
+ name: entry.name,
80
+ slug: entry.slug,
81
+ qualityScore: entry.qualityScore,
82
+ strengths: strengths.slice(0, 4),
83
+ weaknesses: weaknesses.slice(0, 3),
84
+ bestFor: entry.bestFor,
85
+ pricing: pricingSummary,
86
+ pricingModel: entry.pricing.model,
87
+ freeTier: entry.pricing.freeTier ?? "None",
88
+ rateLimits: `${entry.rateLimits.tier}: ${entry.rateLimits.limit}`,
89
+ sdkLanguages: [
90
+ entry.sdk.primaryLanguage,
91
+ ...entry.sdk.otherLanguages,
92
+ ]
93
+ .slice(0, 5)
94
+ .join(", "),
95
+ website: entry.website,
96
+ };
97
+ });
98
+ // Build verdict
99
+ const best = comparison.reduce((a, b) => a.qualityScore >= b.qualityScore ? a : b);
100
+ let verdict;
101
+ if (criteria) {
102
+ const criteriaLower = criteria.toLowerCase();
103
+ // Pricing-focused verdict
104
+ if (criteriaLower.includes("price") ||
105
+ criteriaLower.includes("cost") ||
106
+ criteriaLower.includes("free")) {
107
+ const withFree = comparison.filter((c) => c.freeTier !== "None");
108
+ const cheapest = withFree[0] ?? best;
109
+ verdict = `For ${criteria}, ${cheapest.name} offers the best value with "${cheapest.freeTier}" free tier. ${cheapest.name} is best if staying on a free tier matters most.`;
110
+ }
111
+ else {
112
+ // Generic criteria verdict — defer to quality score
113
+ verdict = `For ${criteria}, ${best.name} (score: ${best.qualityScore}/10) comes out ahead. ${best.bestFor}.`;
114
+ }
115
+ }
116
+ else {
117
+ const names = comparison.map((c) => c.name).join(", ");
118
+ verdict = `Of ${names}: ${best.name} scores highest at ${best.qualityScore}/10 — ${best.bestFor}.`;
119
+ }
120
+ const response = {
121
+ comparison,
122
+ verdict,
123
+ notFound: notFound.length > 0 ? notFound : undefined,
124
+ };
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: JSON.stringify(response, null, 2),
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ catch (err) {
135
+ logger.error("compare_apis error", err);
136
+ return {
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text: JSON.stringify({
141
+ error: "Internal error while comparing APIs",
142
+ details: String(err),
143
+ }),
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ });
149
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MCP Tool: find_best_api
3
+ * Finds and recommends the best API for a given task description.
4
+ * Returns a structured recommendation with quick-start code.
5
+ *
6
+ * Exports: registerFindBestApi
7
+ */
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ export declare function registerFindBestApi(server: McpServer): void;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * MCP Tool: find_best_api
3
+ * Finds and recommends the best API for a given task description.
4
+ * Returns a structured recommendation with quick-start code.
5
+ *
6
+ * Exports: registerFindBestApi
7
+ */
8
+ import { z } from "zod";
9
+ import { findApis } from "../knowledge/search.js";
10
+ import { createLogger } from "../utils/logger.js";
11
+ const logger = createLogger("tool:find-api");
12
+ const inputSchema = {
13
+ task: z.string().min(1).describe("Describe what you need the API to do, e.g. 'send transactional emails with templates'"),
14
+ constraints: z
15
+ .object({
16
+ maxPrice: z
17
+ .string()
18
+ .optional()
19
+ .describe("Maximum acceptable price, e.g. '$50/month'"),
20
+ requiredLanguage: z
21
+ .string()
22
+ .optional()
23
+ .describe("Required SDK language, e.g. 'typescript', 'python'"),
24
+ preferFree: z
25
+ .boolean()
26
+ .optional()
27
+ .describe("Prefer APIs with free tiers"),
28
+ })
29
+ .optional()
30
+ .describe("Optional constraints to filter recommendations"),
31
+ };
32
+ 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 }) => {
34
+ logger.info("find_best_api called", { task, constraints });
35
+ 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
+ };
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: JSON.stringify(response, null, 2),
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ catch (err) {
106
+ logger.error("find_best_api error", err);
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: JSON.stringify({
112
+ error: "Internal error while searching for APIs",
113
+ details: String(err),
114
+ }),
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ });
120
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * MCP Tool: get_api_setup_guide
3
+ * Returns detailed, step-by-step setup instructions for a specific API.
4
+ *
5
+ * Exports: registerGetSetupGuide
6
+ */
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ export declare function registerGetSetupGuide(server: McpServer): void;