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.
- package/Dockerfile +18 -0
- package/README.md +184 -0
- package/claude_desktop_config.json +8 -0
- package/dist/auth.d.ts +38 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +234 -0
- package/dist/server.js.map +1 -0
- package/dist/supabase.d.ts +18 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +86 -0
- package/dist/supabase.js.map +1 -0
- package/dist/tools/compare-prices.d.ts +16 -0
- package/dist/tools/compare-prices.d.ts.map +1 -0
- package/dist/tools/compare-prices.js +152 -0
- package/dist/tools/compare-prices.js.map +1 -0
- package/dist/tools/get-index-benchmarks.d.ts +14 -0
- package/dist/tools/get-index-benchmarks.d.ts.map +1 -0
- package/dist/tools/get-index-benchmarks.js +99 -0
- package/dist/tools/get-index-benchmarks.js.map +1 -0
- package/dist/tools/get-kpis.d.ts +10 -0
- package/dist/tools/get-kpis.d.ts.map +1 -0
- package/dist/tools/get-kpis.js +45 -0
- package/dist/tools/get-kpis.js.map +1 -0
- package/dist/tools/get-market-stats.d.ts +12 -0
- package/dist/tools/get-market-stats.d.ts.map +1 -0
- package/dist/tools/get-market-stats.js +95 -0
- package/dist/tools/get-market-stats.js.map +1 -0
- package/dist/tools/get-model-detail.d.ts +12 -0
- package/dist/tools/get-model-detail.d.ts.map +1 -0
- package/dist/tools/get-model-detail.js +96 -0
- package/dist/tools/get-model-detail.js.map +1 -0
- package/dist/tools/get-vendor-catalog.d.ts +15 -0
- package/dist/tools/get-vendor-catalog.d.ts.map +1 -0
- package/dist/tools/get-vendor-catalog.js +102 -0
- package/dist/tools/get-vendor-catalog.js.map +1 -0
- package/dist/tools/list-vendors.d.ts +13 -0
- package/dist/tools/list-vendors.d.ts.map +1 -0
- package/dist/tools/list-vendors.js +49 -0
- package/dist/tools/list-vendors.js.map +1 -0
- package/dist/tools/search-models.d.ts +22 -0
- package/dist/tools/search-models.d.ts.map +1 -0
- package/dist/tools/search-models.js +128 -0
- package/dist/tools/search-models.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/env.example +17 -0
- package/package.json +52 -0
- package/railway.json +13 -0
- package/smithery.yaml +36 -0
- package/src/auth.ts +101 -0
- package/src/index.ts +94 -0
- package/src/server.ts +278 -0
- package/src/supabase.ts +125 -0
- package/src/tools/compare-prices.ts +175 -0
- package/src/tools/get-index-benchmarks.ts +122 -0
- package/src/tools/get-kpis.ts +62 -0
- package/src/tools/get-market-stats.ts +112 -0
- package/src/tools/get-model-detail.ts +119 -0
- package/src/tools/get-vendor-catalog.ts +121 -0
- package/src/tools/list-vendors.ts +60 -0
- package/src/tools/search-models.ts +146 -0
- package/src/types.ts +145 -0
- 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
|
+
}
|