atom-mcp-server 1.1.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 (73) hide show
  1. package/Dockerfile +18 -0
  2. package/README.md +184 -0
  3. package/claude_desktop_config.json +8 -0
  4. package/dist/auth.d.ts +38 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +75 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +92 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/server.d.ts +3 -0
  13. package/dist/server.d.ts.map +1 -0
  14. package/dist/server.js +234 -0
  15. package/dist/server.js.map +1 -0
  16. package/dist/supabase.d.ts +18 -0
  17. package/dist/supabase.d.ts.map +1 -0
  18. package/dist/supabase.js +86 -0
  19. package/dist/supabase.js.map +1 -0
  20. package/dist/tools/compare-prices.d.ts +16 -0
  21. package/dist/tools/compare-prices.d.ts.map +1 -0
  22. package/dist/tools/compare-prices.js +152 -0
  23. package/dist/tools/compare-prices.js.map +1 -0
  24. package/dist/tools/get-index-benchmarks.d.ts +14 -0
  25. package/dist/tools/get-index-benchmarks.d.ts.map +1 -0
  26. package/dist/tools/get-index-benchmarks.js +99 -0
  27. package/dist/tools/get-index-benchmarks.js.map +1 -0
  28. package/dist/tools/get-kpis.d.ts +10 -0
  29. package/dist/tools/get-kpis.d.ts.map +1 -0
  30. package/dist/tools/get-kpis.js +45 -0
  31. package/dist/tools/get-kpis.js.map +1 -0
  32. package/dist/tools/get-market-stats.d.ts +12 -0
  33. package/dist/tools/get-market-stats.d.ts.map +1 -0
  34. package/dist/tools/get-market-stats.js +95 -0
  35. package/dist/tools/get-market-stats.js.map +1 -0
  36. package/dist/tools/get-model-detail.d.ts +12 -0
  37. package/dist/tools/get-model-detail.d.ts.map +1 -0
  38. package/dist/tools/get-model-detail.js +96 -0
  39. package/dist/tools/get-model-detail.js.map +1 -0
  40. package/dist/tools/get-vendor-catalog.d.ts +15 -0
  41. package/dist/tools/get-vendor-catalog.d.ts.map +1 -0
  42. package/dist/tools/get-vendor-catalog.js +102 -0
  43. package/dist/tools/get-vendor-catalog.js.map +1 -0
  44. package/dist/tools/list-vendors.d.ts +13 -0
  45. package/dist/tools/list-vendors.d.ts.map +1 -0
  46. package/dist/tools/list-vendors.js +49 -0
  47. package/dist/tools/list-vendors.js.map +1 -0
  48. package/dist/tools/search-models.d.ts +22 -0
  49. package/dist/tools/search-models.d.ts.map +1 -0
  50. package/dist/tools/search-models.js +128 -0
  51. package/dist/tools/search-models.js.map +1 -0
  52. package/dist/types.d.ts +119 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +5 -0
  55. package/dist/types.js.map +1 -0
  56. package/env.example +17 -0
  57. package/package.json +52 -0
  58. package/railway.json +13 -0
  59. package/smithery.yaml +36 -0
  60. package/src/auth.ts +101 -0
  61. package/src/index.ts +94 -0
  62. package/src/server.ts +278 -0
  63. package/src/supabase.ts +125 -0
  64. package/src/tools/compare-prices.ts +175 -0
  65. package/src/tools/get-index-benchmarks.ts +122 -0
  66. package/src/tools/get-kpis.ts +62 -0
  67. package/src/tools/get-market-stats.ts +112 -0
  68. package/src/tools/get-model-detail.ts +119 -0
  69. package/src/tools/get-vendor-catalog.ts +121 -0
  70. package/src/tools/list-vendors.ts +60 -0
  71. package/src/tools/search-models.ts +146 -0
  72. package/src/types.ts +145 -0
  73. package/tsconfig.json +19 -0
@@ -0,0 +1,62 @@
1
+ // ============================================================
2
+ // Tool: get_kpis
3
+ // Developer-focused market KPIs from v_pricing_intel.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryView } from "../supabase.js";
8
+ import type { Tier } from "../types.js";
9
+
10
+ export const getKpisSchema = {};
11
+
12
+ export async function handleGetKpis(
13
+ _params: z.infer<z.ZodObject<typeof getKpisSchema>>,
14
+ tier: Tier
15
+ ) {
16
+ // KPIs are public market intelligence — available to all tiers
17
+ // v_pricing_intel returns rows with KPI data
18
+ let kpis: Record<string, unknown>[] = [];
19
+
20
+ try {
21
+ kpis = await queryView<Record<string, unknown>>("v_pricing_intel");
22
+ } catch (error) {
23
+ // If view doesn't exist or fails, return graceful error
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text" as const,
28
+ text: JSON.stringify(
29
+ {
30
+ tool: "get_kpis",
31
+ tier,
32
+ error: "KPI view temporarily unavailable. Try again later.",
33
+ detail: String(error),
34
+ },
35
+ null,
36
+ 2
37
+ ),
38
+ },
39
+ ],
40
+ };
41
+ }
42
+
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text" as const,
47
+ text: JSON.stringify(
48
+ {
49
+ tool: "get_kpis",
50
+ tier,
51
+ description:
52
+ "ATOM Inference Price Index — market-level KPIs derived from 1,600+ SKUs across 40+ vendors.",
53
+ kpis,
54
+ source: "https://a7om.com",
55
+ },
56
+ null,
57
+ 2
58
+ ),
59
+ },
60
+ ],
61
+ };
62
+ }
@@ -0,0 +1,112 @@
1
+ // ============================================================
2
+ // Tool: get_market_stats
3
+ // Aggregate market intelligence from views.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable, queryView } from "../supabase.js";
8
+ import { freeTierNote } from "../auth.js";
9
+ import type { Tier, SummaryStatRow } from "../types.js";
10
+
11
+ export const getMarketStatsSchema = {
12
+ modality: z
13
+ .string()
14
+ .optional()
15
+ .describe("Optionally focus on a specific modality: Text, Image, Audio, Video, etc."),
16
+ };
17
+
18
+ export async function handleGetMarketStats(
19
+ params: z.infer<z.ZodObject<typeof getMarketStatsSchema>>,
20
+ tier: Tier
21
+ ) {
22
+ // Get summary stats from view — returns rows of {metric, value}
23
+ const statRows = await queryView<SummaryStatRow>("v_summary_stats");
24
+
25
+ // Convert to a lookup object
26
+ const coverage: Record<string, string> = {};
27
+ for (const row of statRows) {
28
+ coverage[row.metric] = row.value;
29
+ }
30
+
31
+ // Get price distribution for the requested modality
32
+ const priceFilters: string[] = [];
33
+ if (params.modality) priceFilters.push(`modality=ilike.*${params.modality}*`);
34
+ priceFilters.push("normalized_price=gt.0");
35
+
36
+ const skus = await queryTable<Record<string, unknown>>("sku_index", priceFilters, {
37
+ select: "normalized_price,modality,direction,vendor_name",
38
+ });
39
+
40
+ const prices = skus
41
+ .map((s) => s.normalized_price as number)
42
+ .filter((p) => p !== null && p > 0)
43
+ .sort((a, b) => a - b);
44
+
45
+ const median = prices.length > 0
46
+ ? prices[Math.floor(prices.length / 2)]
47
+ : null;
48
+ const mean = prices.length > 0
49
+ ? +(prices.reduce((a, b) => a + b, 0) / prices.length).toFixed(6)
50
+ : null;
51
+
52
+ // Vendor distribution
53
+ const vendorCounts: Record<string, number> = {};
54
+ for (const s of skus) {
55
+ const v = s.vendor_name as string;
56
+ vendorCounts[v] = (vendorCounts[v] || 0) + 1;
57
+ }
58
+
59
+ // Modality distribution
60
+ const modalityCounts: Record<string, number> = {};
61
+ for (const s of skus) {
62
+ const m = s.modality as string;
63
+ modalityCounts[m] = (modalityCounts[m] || 0) + 1;
64
+ }
65
+
66
+ // Direction distribution
67
+ const directionCounts: Record<string, number> = {};
68
+ for (const s of skus) {
69
+ const d = s.direction as string;
70
+ directionCounts[d] = (directionCounts[d] || 0) + 1;
71
+ }
72
+
73
+ const response: Record<string, unknown> = {
74
+ tool: "get_market_stats",
75
+ tier,
76
+ coverage,
77
+ price_distribution: {
78
+ modality_filter: params.modality || "All",
79
+ total_skus: prices.length,
80
+ median_price: median,
81
+ mean_price: mean,
82
+ min_price: prices.length > 0 ? prices[0] : null,
83
+ max_price: prices.length > 0 ? prices[prices.length - 1] : null,
84
+ p25: prices.length > 0 ? prices[Math.floor(prices.length * 0.25)] : null,
85
+ p75: prices.length > 0 ? prices[Math.floor(prices.length * 0.75)] : null,
86
+ },
87
+ modality_breakdown: modalityCounts,
88
+ direction_breakdown: directionCounts,
89
+ };
90
+
91
+ // Vendor breakdown only for paid tier
92
+ if (tier === "paid") {
93
+ response.vendor_breakdown = vendorCounts;
94
+ } else {
95
+ response.vendor_count = Object.keys(vendorCounts).length;
96
+ response.upgrade_message =
97
+ "Vendor-level breakdown requires ATOM MCP Pro ($49/mo). Visit https://a7om.com/mcp";
98
+ }
99
+
100
+ const content: { type: "text"; text: string }[] = [
101
+ {
102
+ type: "text" as const,
103
+ text: JSON.stringify(response, null, 2),
104
+ },
105
+ ];
106
+
107
+ if (tier === "free") {
108
+ content.push(freeTierNote("Vendor-level breakdown and granular market segmentation"));
109
+ }
110
+
111
+ return { content };
112
+ }
@@ -0,0 +1,119 @@
1
+ // ============================================================
2
+ // Tool: get_model_detail
3
+ // Deep dive on a single model — specs + pricing across vendors.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import { gateResults, freeTierNote } from "../auth.js";
9
+ import type { Tier, ModelRegistry, SkuIndex } from "../types.js";
10
+
11
+ export const getModelDetailSchema = {
12
+ model_name: z
13
+ .string()
14
+ .describe("Model name to look up, e.g. 'GPT-4o', 'Claude Sonnet 4.5', 'Llama 3.1 70B'"),
15
+ };
16
+
17
+ export async function handleGetModelDetail(
18
+ params: z.infer<z.ZodObject<typeof getModelDetailSchema>>,
19
+ tier: Tier
20
+ ) {
21
+ // Find model in registry (fuzzy match)
22
+ const models = await queryTable<ModelRegistry>("model_registry", [
23
+ `model_name=ilike.*${params.model_name}*`,
24
+ ], {
25
+ limit: 5,
26
+ });
27
+
28
+ if (models.length === 0) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text" as const,
33
+ text: JSON.stringify({
34
+ tool: "get_model_detail",
35
+ error: `No model found matching '${params.model_name}'. Try a partial name like 'GPT-4' or 'Claude'.`,
36
+ }),
37
+ },
38
+ ],
39
+ };
40
+ }
41
+
42
+ const model = models[0];
43
+
44
+ // Get all SKUs for this model across vendors
45
+ const skus = await queryTable<SkuIndex>("sku_index", [
46
+ `model_id=eq.${model.model_id}`,
47
+ ], {
48
+ select: "sku_id,vendor_name,model_name,modality,modality_subtype,direction,normalized_price,normalized_price_unit,billing_method",
49
+ order: "normalized_price.asc",
50
+ });
51
+
52
+ // Build response
53
+ const modelSpecs = {
54
+ model_id: model.model_id,
55
+ model_name: model.model_name,
56
+ creator: model.creator,
57
+ model_family: model.model_family,
58
+ open_source: model.open_source,
59
+ parameter_count: model.parameter_count,
60
+ context_window: model.context_window,
61
+ max_output_tokens: model.max_output_tokens,
62
+ training_cutoff: model.training_cutoff,
63
+ modality_input: model.modality_input,
64
+ modality_output: model.modality_output,
65
+ tool_calling: model.tool_calling,
66
+ json_mode: model.json_mode,
67
+ streaming: model.streaming,
68
+ source_url: model.source_url,
69
+ };
70
+
71
+ let pricing: unknown;
72
+
73
+ if (tier === "paid") {
74
+ pricing = skus;
75
+ } else {
76
+ // Free tier: show count and redacted sample
77
+ const vendors = [...new Set(skus.map((s) => s.vendor_name))];
78
+ pricing = {
79
+ total_skus: skus.length,
80
+ vendors_offering: vendors.length,
81
+ modalities: [...new Set(skus.map((s) => s.modality))],
82
+ directions: [...new Set(skus.map((s) => s.direction))],
83
+ sample: gateResults(
84
+ skus.slice(0, 3) as unknown as Record<string, unknown>[],
85
+ "free"
86
+ ),
87
+ upgrade_message:
88
+ "Full vendor-by-vendor pricing requires ATOM MCP Pro ($49/mo). Visit https://a7om.com/mcp",
89
+ };
90
+ }
91
+
92
+ // Additional matches (other models with similar names)
93
+ const additionalMatches = models.length > 1
94
+ ? models.slice(1).map((m) => m.model_name)
95
+ : [];
96
+
97
+ const content: { type: "text"; text: string }[] = [
98
+ {
99
+ type: "text" as const,
100
+ text: JSON.stringify(
101
+ {
102
+ tool: "get_model_detail",
103
+ tier,
104
+ model_specs: modelSpecs,
105
+ pricing,
106
+ additional_matches: additionalMatches,
107
+ },
108
+ null,
109
+ 2
110
+ ),
111
+ },
112
+ ];
113
+
114
+ if (tier === "free") {
115
+ content.push(freeTierNote("Detailed per-vendor pricing for this model"));
116
+ }
117
+
118
+ return { content };
119
+ }
@@ -0,0 +1,121 @@
1
+ // ============================================================
2
+ // Tool: get_vendor_catalog
3
+ // Everything a vendor offers: models, modalities, prices + metadata.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import { gateResults, freeTierNote } from "../auth.js";
9
+ import type { Tier, VendorRegistry } from "../types.js";
10
+
11
+ export const getVendorCatalogSchema = {
12
+ vendor: z
13
+ .string()
14
+ .describe("Vendor name, e.g. 'OpenAI', 'Together AI', 'Amazon Bedrock'"),
15
+ modality: z
16
+ .string()
17
+ .optional()
18
+ .describe("Optionally filter by modality: Text, Image, Audio, Video, Voice, Multimodal"),
19
+ direction: z
20
+ .enum(["Input", "Output", "Cached Input"])
21
+ .optional()
22
+ .describe("Optionally filter by pricing direction"),
23
+ limit: z
24
+ .coerce.number()
25
+ .int()
26
+ .min(1)
27
+ .max(200)
28
+ .default(50)
29
+ .describe("Maximum results (default 50)"),
30
+ };
31
+
32
+ export async function handleGetVendorCatalog(
33
+ params: z.infer<z.ZodObject<typeof getVendorCatalogSchema>>,
34
+ tier: Tier
35
+ ) {
36
+ // Get vendor metadata
37
+ const vendors = await queryTable<VendorRegistry>("vendor_registry", [
38
+ `vendor_name=ilike.*${params.vendor}*`,
39
+ ]);
40
+
41
+ if (vendors.length === 0) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text" as const,
46
+ text: JSON.stringify({
47
+ tool: "get_vendor_catalog",
48
+ error: `No vendor found matching '${params.vendor}'. Use list_vendors to see all available vendors.`,
49
+ }),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ const vendor = vendors[0];
56
+
57
+ // Get all SKUs for this vendor
58
+ const skuFilters: string[] = [
59
+ `vendor_name=ilike.*${params.vendor}*`,
60
+ ];
61
+ if (params.modality) skuFilters.push(`modality=ilike.*${params.modality}*`);
62
+ if (params.direction) skuFilters.push(`direction=eq.${params.direction}`);
63
+
64
+ const skus = await queryTable<Record<string, unknown>>("sku_index", skuFilters, {
65
+ select: "sku_id,vendor_name,model_name,modality,modality_subtype,direction,normalized_price,normalized_price_unit,billing_method",
66
+ order: "model_name.asc,direction.asc",
67
+ limit: params.limit,
68
+ });
69
+
70
+ // Build catalog summary
71
+ const models = [...new Set(skus.map((s) => s.model_name as string))];
72
+ const modalities = [...new Set(skus.map((s) => s.modality as string))];
73
+
74
+ const catalogSummary = {
75
+ vendor_name: vendor.vendor_name,
76
+ country: vendor.country,
77
+ region: vendor.region,
78
+ pricing_page: vendor.pricing_page_url,
79
+ website: vendor.vendor_url,
80
+ total_models: models.length,
81
+ total_skus: skus.length,
82
+ modalities,
83
+ };
84
+
85
+ let catalog: unknown;
86
+
87
+ if (tier === "paid") {
88
+ catalog = {
89
+ summary: catalogSummary,
90
+ skus,
91
+ };
92
+ } else {
93
+ catalog = {
94
+ summary: catalogSummary,
95
+ sample: gateResults(skus.slice(0, 3), "free"),
96
+ upgrade_message:
97
+ "Full catalog with pricing requires ATOM MCP Pro ($49/mo). Visit https://a7om.com/mcp",
98
+ };
99
+ }
100
+
101
+ const content: { type: "text"; text: string }[] = [
102
+ {
103
+ type: "text" as const,
104
+ text: JSON.stringify(
105
+ {
106
+ tool: "get_vendor_catalog",
107
+ tier,
108
+ catalog,
109
+ },
110
+ null,
111
+ 2
112
+ ),
113
+ },
114
+ ];
115
+
116
+ if (tier === "free") {
117
+ content.push(freeTierNote("Full vendor catalog with all SKU-level pricing"));
118
+ }
119
+
120
+ return { content };
121
+ }
@@ -0,0 +1,60 @@
1
+ // ============================================================
2
+ // Tool: list_vendors
3
+ // All tracked vendors with metadata. Simple discovery tool.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import type { Tier, VendorRegistry } from "../types.js";
9
+
10
+ export const listVendorsSchema = {
11
+ region: z
12
+ .string()
13
+ .optional()
14
+ .describe("Optionally filter by region: 'North America', 'Europe', 'Asia', etc."),
15
+ country: z
16
+ .string()
17
+ .optional()
18
+ .describe("Optionally filter by country, e.g. 'United States', 'China', 'France'"),
19
+ };
20
+
21
+ export async function handleListVendors(
22
+ params: z.infer<z.ZodObject<typeof listVendorsSchema>>,
23
+ tier: Tier
24
+ ) {
25
+ const filters: string[] = [];
26
+ if (params.region) filters.push(`region=ilike.*${params.region}*`);
27
+ if (params.country) filters.push(`country=ilike.*${params.country}*`);
28
+
29
+ const vendors = await queryTable<VendorRegistry>("vendor_registry", filters, {
30
+ order: "vendor_name.asc",
31
+ });
32
+
33
+ // Vendor list is public info — no gating needed.
34
+ const formatted = vendors.map((v) => ({
35
+ vendor_id: v.vendor_id,
36
+ name: v.vendor_name,
37
+ country: v.country,
38
+ region: v.region,
39
+ pricing_page: v.pricing_page_url,
40
+ website: v.vendor_url,
41
+ }));
42
+
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text" as const,
47
+ text: JSON.stringify(
48
+ {
49
+ tool: "list_vendors",
50
+ tier,
51
+ total: formatted.length,
52
+ vendors: formatted,
53
+ },
54
+ null,
55
+ 2
56
+ ),
57
+ },
58
+ ],
59
+ };
60
+ }
@@ -0,0 +1,146 @@
1
+ // ============================================================
2
+ // Tool: search_models
3
+ // Multi-filter search across the SKU index.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import { gateResults, buildFreeTierSummary, freeTierNote } from "../auth.js";
9
+ import type { Tier, SkuIndex, ModelRegistry } from "../types.js";
10
+
11
+ export const searchModelsSchema = {
12
+ modality: z
13
+ .string()
14
+ .optional()
15
+ .describe("Filter by modality: Text, Image, Audio, Video, Voice, Multimodal, Embedding"),
16
+ vendor: z
17
+ .string()
18
+ .optional()
19
+ .describe("Filter by vendor name, e.g. 'OpenAI', 'Anthropic'"),
20
+ creator: z
21
+ .string()
22
+ .optional()
23
+ .describe("Filter by model creator/developer"),
24
+ model_family: z
25
+ .string()
26
+ .optional()
27
+ .describe("Filter by model family, e.g. 'GPT-4o', 'Claude 3.5'"),
28
+ open_source: z
29
+ .string()
30
+ .optional()
31
+ .describe("Filter by open-source status: 'true' or 'false'"),
32
+ direction: z
33
+ .enum(["Input", "Output", "Cached Input"])
34
+ .optional()
35
+ .describe("Filter by pricing direction"),
36
+ max_price: z
37
+ .coerce.number()
38
+ .optional()
39
+ .describe("Maximum normalized price (USD per unit)"),
40
+ min_context_window: z
41
+ .coerce.number()
42
+ .int()
43
+ .optional()
44
+ .describe("Minimum context window in tokens"),
45
+ min_parameter_count: z
46
+ .string()
47
+ .optional()
48
+ .describe("Minimum parameter count, e.g. '7B', '70B'"),
49
+ limit: z
50
+ .coerce.number()
51
+ .int()
52
+ .min(1)
53
+ .max(100)
54
+ .default(20)
55
+ .describe("Maximum results to return (default 20)"),
56
+ offset: z
57
+ .coerce.number()
58
+ .int()
59
+ .min(0)
60
+ .default(0)
61
+ .describe("Offset for pagination"),
62
+ };
63
+
64
+ export async function handleSearchModels(
65
+ params: z.infer<z.ZodObject<typeof searchModelsSchema>>,
66
+ tier: Tier
67
+ ) {
68
+ // Build SKU-level filters
69
+ const skuFilters: string[] = [];
70
+ if (params.modality) skuFilters.push(`modality=ilike.*${params.modality}*`);
71
+ if (params.vendor) skuFilters.push(`vendor_name=ilike.*${params.vendor}*`);
72
+ if (params.direction) skuFilters.push(`direction=eq.${params.direction}`);
73
+ if (params.max_price !== undefined)
74
+ skuFilters.push(`normalized_price=lte.${params.max_price}`);
75
+ skuFilters.push("normalized_price=gt.0");
76
+
77
+ // Query SKU index
78
+ let skus = await queryTable<SkuIndex>("sku_index", skuFilters, {
79
+ select:
80
+ "sku_id,model_id,vendor_id,vendor_name,model_name,modality,modality_subtype,direction,normalized_price,normalized_price_unit,billing_method",
81
+ order: "normalized_price.asc",
82
+ limit: params.limit + 50, // extra buffer for model-level filtering
83
+ offset: params.offset,
84
+ });
85
+
86
+ // Apply model-level filters if needed
87
+ if (params.creator || params.model_family || params.open_source || params.min_context_window) {
88
+ const modelFilters: string[] = [];
89
+ if (params.creator) modelFilters.push(`creator=ilike.*${params.creator}*`);
90
+ if (params.model_family) modelFilters.push(`model_family=ilike.*${params.model_family}*`);
91
+ if (params.open_source !== undefined) {
92
+ const boolVal = params.open_source === "true";
93
+ modelFilters.push(`open_source=is.${boolVal}`);
94
+ }
95
+ if (params.min_context_window)
96
+ modelFilters.push(`context_window=gte.${params.min_context_window}`);
97
+
98
+ const models = await queryTable<ModelRegistry>("model_registry", modelFilters, {
99
+ select: "model_id",
100
+ });
101
+
102
+ const modelIds = new Set(models.map((m) => m.model_id));
103
+ skus = skus.filter((s) => modelIds.has(s.model_id));
104
+ }
105
+
106
+ // Trim to requested limit
107
+ const trimmed = skus.slice(0, params.limit);
108
+
109
+ // Gate results based on tier
110
+ const gated = gateResults(
111
+ trimmed as unknown as Record<string, unknown>[],
112
+ tier
113
+ );
114
+
115
+ const content: { type: "text"; text: string }[] = [
116
+ {
117
+ type: "text" as const,
118
+ text: JSON.stringify(
119
+ {
120
+ tool: "search_models",
121
+ tier,
122
+ filters: {
123
+ modality: params.modality,
124
+ vendor: params.vendor,
125
+ creator: params.creator,
126
+ model_family: params.model_family,
127
+ open_source: params.open_source,
128
+ direction: params.direction,
129
+ max_price: params.max_price,
130
+ },
131
+ total_results: skus.length,
132
+ showing: trimmed.length,
133
+ results: gated,
134
+ },
135
+ null,
136
+ 2
137
+ ),
138
+ },
139
+ ];
140
+
141
+ if (tier === "free") {
142
+ content.push(freeTierNote("Full model search results with exact pricing"));
143
+ }
144
+
145
+ return { content };
146
+ }