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
package/src/server.ts ADDED
@@ -0,0 +1,278 @@
1
+ // ============================================================
2
+ // ATOM MCP Server — Tool Registration
3
+ // ============================================================
4
+ // Registers all 8 tools with the MCP SDK's McpServer.
5
+ // Each tool handler resolves the caller's tier from the API key
6
+ // passed via the _atom_api_key field.
7
+ // ============================================================
8
+
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { z } from "zod";
11
+ import { resolveTier } from "./auth.js";
12
+
13
+ // Tool imports
14
+ import { searchModelsSchema, handleSearchModels } from "./tools/search-models.js";
15
+ import { getModelDetailSchema, handleGetModelDetail } from "./tools/get-model-detail.js";
16
+ import { comparePricesSchema, handleComparePrices } from "./tools/compare-prices.js";
17
+ import { getVendorCatalogSchema, handleGetVendorCatalog } from "./tools/get-vendor-catalog.js";
18
+ import { getMarketStatsSchema, handleGetMarketStats } from "./tools/get-market-stats.js";
19
+ import { getIndexBenchmarksSchema, handleGetIndexBenchmarks } from "./tools/get-index-benchmarks.js";
20
+ import { getKpisSchema, handleGetKpis } from "./tools/get-kpis.js";
21
+ import { listVendorsSchema, handleListVendors } from "./tools/list-vendors.js";
22
+
23
+ // Common API key field injected into every tool's schema
24
+ const apiKeyField = {
25
+ _atom_api_key: z
26
+ .string()
27
+ .optional()
28
+ .describe("Your ATOM API key for full access. Omit for free tier (redacted data)."),
29
+ };
30
+
31
+ export function createServer(): McpServer {
32
+ const server = new McpServer({
33
+ name: "atom-mcp-server",
34
+ version: "1.0.0",
35
+ });
36
+
37
+ // ----------------------------------------------------------
38
+ // 1. search_models
39
+ // ----------------------------------------------------------
40
+ server.registerTool(
41
+ "search_models",
42
+ {
43
+ title: "Search AI Models",
44
+ description: `Search and filter AI inference models across 40+ vendors and 1,600+ SKUs.
45
+
46
+ Query by modality (Text, Image, Audio, Video, Multimodal), vendor, creator, model family, open-source status, price range, context window, and parameter count.
47
+
48
+ Returns matching models with pricing. Free tier shows count + price range; paid tier shows full details.
49
+
50
+ Examples:
51
+ - "Find open-source text models under $1/M tokens" → open_source=true, modality="Text", max_price=0.001
52
+ - "What multimodal models does Google offer?" → vendor="Google", modality="Multimodal"
53
+ - "Models with 128K+ context window" → min_context_window=128000`,
54
+ inputSchema: { ...searchModelsSchema, ...apiKeyField },
55
+ annotations: {
56
+ readOnlyHint: true,
57
+ destructiveHint: false,
58
+ idempotentHint: true,
59
+ openWorldHint: false,
60
+ },
61
+ },
62
+ async (params) => {
63
+ const tier = resolveTier(params._atom_api_key);
64
+ return handleSearchModels(params, tier);
65
+ }
66
+ );
67
+
68
+ // ----------------------------------------------------------
69
+ // 2. get_model_detail
70
+ // ----------------------------------------------------------
71
+ server.registerTool(
72
+ "get_model_detail",
73
+ {
74
+ title: "Get Model Details",
75
+ description: `Deep dive on a single AI model: technical specs + pricing across all vendors.
76
+
77
+ Returns model_registry data (context window, parameters, open-source status, training cutoff, model family) plus all SKU pricing across every vendor that offers this model.
78
+
79
+ Examples:
80
+ - "Tell me everything about GPT-4o" → model_name="GPT-4o"
81
+ - "Claude Sonnet 4.5 specs and pricing" → model_name="Claude Sonnet 4.5"`,
82
+ inputSchema: { ...getModelDetailSchema, ...apiKeyField },
83
+ annotations: {
84
+ readOnlyHint: true,
85
+ destructiveHint: false,
86
+ idempotentHint: true,
87
+ openWorldHint: false,
88
+ },
89
+ },
90
+ async (params) => {
91
+ const tier = resolveTier(params._atom_api_key);
92
+ return handleGetModelDetail(params, tier);
93
+ }
94
+ );
95
+
96
+ // ----------------------------------------------------------
97
+ // 3. compare_prices
98
+ // ----------------------------------------------------------
99
+ server.registerTool(
100
+ "compare_prices",
101
+ {
102
+ title: "Compare Prices Across Vendors",
103
+ description: `Cross-vendor price comparison for a specific model or model family.
104
+
105
+ Shows the same model (or family) priced across different vendors, sorted cheapest first. Essential for cost optimization and vendor selection.
106
+
107
+ Examples:
108
+ - "Compare Llama 3.1 70B pricing across vendors" → model_name="Llama 3.1 70B"
109
+ - "Cheapest GPT-4 family output pricing" → model_family="GPT-4", direction="Output"
110
+ - "Claude pricing comparison" → model_family="Claude"`,
111
+ inputSchema: { ...comparePricesSchema, ...apiKeyField },
112
+ annotations: {
113
+ readOnlyHint: true,
114
+ destructiveHint: false,
115
+ idempotentHint: true,
116
+ openWorldHint: false,
117
+ },
118
+ },
119
+ async (params) => {
120
+ const tier = resolveTier(params._atom_api_key);
121
+ return handleComparePrices(params, tier);
122
+ }
123
+ );
124
+
125
+ // ----------------------------------------------------------
126
+ // 4. get_vendor_catalog
127
+ // ----------------------------------------------------------
128
+ server.registerTool(
129
+ "get_vendor_catalog",
130
+ {
131
+ title: "Get Vendor Catalog",
132
+ description: `Full catalog for a specific vendor: all models, modalities, and pricing.
133
+
134
+ Returns vendor metadata (country, region, pricing page URL) plus every model and SKU they offer.
135
+
136
+ Examples:
137
+ - "What does Together AI sell?" → vendor="Together AI"
138
+ - "OpenAI's text model pricing" → vendor="OpenAI", modality="Text"
139
+ - "Amazon Bedrock catalog" → vendor="Amazon Bedrock"`,
140
+ inputSchema: { ...getVendorCatalogSchema, ...apiKeyField },
141
+ annotations: {
142
+ readOnlyHint: true,
143
+ destructiveHint: false,
144
+ idempotentHint: true,
145
+ openWorldHint: false,
146
+ },
147
+ },
148
+ async (params) => {
149
+ const tier = resolveTier(params._atom_api_key);
150
+ return handleGetVendorCatalog(params, tier);
151
+ }
152
+ );
153
+
154
+ // ----------------------------------------------------------
155
+ // 5. get_market_stats
156
+ // ----------------------------------------------------------
157
+ server.registerTool(
158
+ "get_market_stats",
159
+ {
160
+ title: "Get Market Statistics",
161
+ description: `Aggregate AI inference market intelligence.
162
+
163
+ Returns total vendor/model/SKU counts, price distribution (median, mean, quartiles, min/max), and modality breakdown. Optionally filter by modality.
164
+
165
+ Examples:
166
+ - "AI inference market overview" → (no params)
167
+ - "Text model pricing statistics" → modality="Text"
168
+ - "Image generation market stats" → modality="Image"`,
169
+ inputSchema: { ...getMarketStatsSchema, ...apiKeyField },
170
+ annotations: {
171
+ readOnlyHint: true,
172
+ destructiveHint: false,
173
+ idempotentHint: true,
174
+ openWorldHint: false,
175
+ },
176
+ },
177
+ async (params) => {
178
+ const tier = resolveTier(params._atom_api_key);
179
+ return handleGetMarketStats(params, tier);
180
+ }
181
+ );
182
+
183
+ // ----------------------------------------------------------
184
+ // 6. get_index_benchmarks
185
+ // ----------------------------------------------------------
186
+ server.registerTool(
187
+ "get_index_benchmarks",
188
+ {
189
+ title: "Get AIPI Index Benchmarks",
190
+ description: `AIPI (ATOM Inference Price Index) — chained matched-model price benchmarks for AI inference.
191
+
192
+ Returns benchmark prices across index families (Text, Image, Audio, Video, Multimodal, Composite) with input, cached, and output pricing per period.
193
+
194
+ These are market-wide benchmarks, not individual vendor prices. Use them to understand where the market is and how it's moving.
195
+
196
+ Fully public — available to all tiers.
197
+
198
+ Examples:
199
+ - "What's the current benchmark for text inference?" → index_category="Text"
200
+ - "Show me all AIPI indexes" → (no params)
201
+ - "AIPI-TXT-GLB history" → index_code="AIPI-TXT-GLB"`,
202
+ inputSchema: { ...getIndexBenchmarksSchema, ...apiKeyField },
203
+ annotations: {
204
+ readOnlyHint: true,
205
+ destructiveHint: false,
206
+ idempotentHint: true,
207
+ openWorldHint: false,
208
+ },
209
+ },
210
+ async (params) => {
211
+ const tier = resolveTier(params._atom_api_key);
212
+ return handleGetIndexBenchmarks(params, tier);
213
+ }
214
+ );
215
+
216
+ // ----------------------------------------------------------
217
+ // 7. get_kpis
218
+ // ----------------------------------------------------------
219
+ server.registerTool(
220
+ "get_kpis",
221
+ {
222
+ title: "Get Market KPIs",
223
+ description: `ATOM Inference Price Index (AIPI) market-level KPIs.
224
+
225
+ Returns 6 key performance indicators derived from 1,600+ SKUs:
226
+ - Output Premium: how much more output tokens cost vs input
227
+ - Caching Savings: average discount for cached input pricing
228
+ - Open Source Advantage: price difference between open-source and proprietary
229
+ - Context Cost Curve: price multiplier for larger context windows
230
+ - Caching Availability: % of models offering cached pricing
231
+ - Size Spread: price ratio between largest and smallest models
232
+
233
+ These KPIs are available to all tiers — they demonstrate ATOM's market intelligence.`,
234
+ inputSchema: { ...getKpisSchema, ...apiKeyField },
235
+ annotations: {
236
+ readOnlyHint: true,
237
+ destructiveHint: false,
238
+ idempotentHint: true,
239
+ openWorldHint: false,
240
+ },
241
+ },
242
+ async (params) => {
243
+ const tier = resolveTier(params._atom_api_key);
244
+ return handleGetKpis(params, tier);
245
+ }
246
+ );
247
+
248
+ // ----------------------------------------------------------
249
+ // 8. list_vendors
250
+ // ----------------------------------------------------------
251
+ server.registerTool(
252
+ "list_vendors",
253
+ {
254
+ title: "List All Vendors",
255
+ description: `List all 41 AI inference vendors tracked by ATOM.
256
+
257
+ Returns vendor name, country, region, and pricing page URL. Optionally filter by region or country.
258
+
259
+ Examples:
260
+ - "List all vendors" → (no params)
261
+ - "European AI vendors" → region="Europe"
262
+ - "Chinese AI vendors" → country="China"`,
263
+ inputSchema: { ...listVendorsSchema, ...apiKeyField },
264
+ annotations: {
265
+ readOnlyHint: true,
266
+ destructiveHint: false,
267
+ idempotentHint: true,
268
+ openWorldHint: false,
269
+ },
270
+ },
271
+ async (params) => {
272
+ const tier = resolveTier(params._atom_api_key);
273
+ return handleListVendors(params, tier);
274
+ }
275
+ );
276
+
277
+ return server;
278
+ }
@@ -0,0 +1,125 @@
1
+ // ============================================================
2
+ // ATOM MCP Server — Supabase REST Client
3
+ // ============================================================
4
+ // Lightweight fetch-based client for Supabase PostgREST API.
5
+ // No Supabase JS SDK dependency — keeps the binary small.
6
+ // ============================================================
7
+
8
+ const SUPABASE_URL = process.env.SUPABASE_URL || "";
9
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "";
10
+
11
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
12
+ console.error(
13
+ "ATOM MCP: SUPABASE_URL and SUPABASE_ANON_KEY must be set in environment variables."
14
+ );
15
+ }
16
+
17
+ const BASE = `${SUPABASE_URL}/rest/v1`;
18
+
19
+ const headers: Record<string, string> = {
20
+ apikey: SUPABASE_ANON_KEY,
21
+ Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
22
+ "Content-Type": "application/json",
23
+ Prefer: "return=representation",
24
+ };
25
+
26
+ // ------------------------------------------------------------
27
+ // Generic query builder
28
+ // ------------------------------------------------------------
29
+
30
+ interface QueryOptions {
31
+ table: string;
32
+ select?: string;
33
+ filters?: string[]; // PostgREST filter strings e.g. "vendor_name=eq.OpenAI"
34
+ order?: string; // e.g. "normalized_price.asc"
35
+ limit?: number;
36
+ offset?: number;
37
+ count?: "exact" | "planned" | "estimated";
38
+ }
39
+
40
+ export async function query<T>(opts: QueryOptions): Promise<{ data: T[]; count: number | null }> {
41
+ const params = new URLSearchParams();
42
+
43
+ if (opts.select) params.set("select", opts.select);
44
+ if (opts.order) params.set("order", opts.order);
45
+ if (opts.limit) params.set("limit", String(opts.limit));
46
+ if (opts.offset) params.set("offset", String(opts.offset));
47
+
48
+ let url = `${BASE}/${opts.table}?${params.toString()}`;
49
+
50
+ // Append filters directly (PostgREST uses column=operator.value syntax)
51
+ if (opts.filters && opts.filters.length > 0) {
52
+ for (const f of opts.filters) {
53
+ url += `&${f}`;
54
+ }
55
+ }
56
+
57
+ const reqHeaders = { ...headers };
58
+ if (opts.count) {
59
+ reqHeaders["Prefer"] = `count=${opts.count}`;
60
+ }
61
+
62
+ const response = await fetch(url, { headers: reqHeaders });
63
+
64
+ if (!response.ok) {
65
+ const body = await response.text();
66
+ throw new Error(`Supabase query failed (${response.status}): ${body}`);
67
+ }
68
+
69
+ const data = (await response.json()) as T[];
70
+ const contentRange = response.headers.get("content-range");
71
+ let count: number | null = null;
72
+ if (contentRange) {
73
+ const match = contentRange.match(/\/(\d+|\*)/);
74
+ if (match && match[1] !== "*") {
75
+ count = parseInt(match[1], 10);
76
+ }
77
+ }
78
+
79
+ return { data, count };
80
+ }
81
+
82
+ // ------------------------------------------------------------
83
+ // RPC call helper (for Supabase functions / views)
84
+ // ------------------------------------------------------------
85
+
86
+ export async function rpc<T>(functionName: string, body?: Record<string, unknown>): Promise<T[]> {
87
+ const url = `${SUPABASE_URL}/rest/v1/rpc/${functionName}`;
88
+ const response = await fetch(url, {
89
+ method: "POST",
90
+ headers,
91
+ body: body ? JSON.stringify(body) : undefined,
92
+ });
93
+
94
+ if (!response.ok) {
95
+ const text = await response.text();
96
+ throw new Error(`Supabase RPC ${functionName} failed (${response.status}): ${text}`);
97
+ }
98
+
99
+ return (await response.json()) as T[];
100
+ }
101
+
102
+ // ------------------------------------------------------------
103
+ // Convenience helpers for common tables
104
+ // ------------------------------------------------------------
105
+
106
+ export async function queryTable<T>(
107
+ table: string,
108
+ filters: string[] = [],
109
+ options: Partial<QueryOptions> = {}
110
+ ): Promise<T[]> {
111
+ const result = await query<T>({
112
+ table,
113
+ filters,
114
+ ...options,
115
+ });
116
+ return result.data;
117
+ }
118
+
119
+ export async function queryView<T>(
120
+ viewName: string,
121
+ filters: string[] = [],
122
+ options: Partial<QueryOptions> = {}
123
+ ): Promise<T[]> {
124
+ return queryTable<T>(viewName, filters, options);
125
+ }
@@ -0,0 +1,175 @@
1
+ // ============================================================
2
+ // Tool: compare_prices
3
+ // Cross-vendor price comparison for a model or model family.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import { gateResults, freeTierNote } from "../auth.js";
9
+ import type { Tier, SkuIndex } from "../types.js";
10
+
11
+ export const comparePricesSchema = {
12
+ model_name: z
13
+ .string()
14
+ .optional()
15
+ .describe("Model name to compare prices for, e.g. 'GPT-4o', 'Llama 3.1 70B'"),
16
+ model_family: z
17
+ .string()
18
+ .optional()
19
+ .describe("Model family to compare, e.g. 'GPT-4o', 'Claude 3.5'"),
20
+ direction: z
21
+ .enum(["Input", "Output", "Cached Input"])
22
+ .optional()
23
+ .describe("Filter by pricing direction"),
24
+ modality: z
25
+ .string()
26
+ .optional()
27
+ .describe("Filter by modality: Text, Image, Audio, etc."),
28
+ limit: z
29
+ .coerce.number()
30
+ .int()
31
+ .min(1)
32
+ .max(100)
33
+ .default(50)
34
+ .describe("Maximum results (default 50)"),
35
+ };
36
+
37
+ export async function handleComparePrices(
38
+ params: z.infer<z.ZodObject<typeof comparePricesSchema>>,
39
+ tier: Tier
40
+ ) {
41
+ if (!params.model_name && !params.model_family) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text" as const,
46
+ text: JSON.stringify({
47
+ tool: "compare_prices",
48
+ error: "Please provide either model_name or model_family to compare.",
49
+ }),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ const filters: string[] = [];
56
+ if (params.model_name) filters.push(`model_name=ilike.*${params.model_name}*`);
57
+ if (params.direction) filters.push(`direction=eq.${params.direction}`);
58
+ if (params.modality) filters.push(`modality=ilike.*${params.modality}*`);
59
+ filters.push("normalized_price=gt.0");
60
+
61
+ let skus = await queryTable<SkuIndex>("sku_index", filters, {
62
+ select: "sku_id,vendor_name,model_name,modality,direction,normalized_price,normalized_price_unit,billing_method",
63
+ order: "normalized_price.asc",
64
+ limit: params.limit,
65
+ });
66
+
67
+ // If model_family was provided, filter by model registry
68
+ if (params.model_family && !params.model_name) {
69
+ const models = await queryTable<{ model_id: string }>("model_registry", [
70
+ `model_family=ilike.*${params.model_family}*`,
71
+ ], {
72
+ select: "model_id",
73
+ });
74
+ const modelIds = new Set(models.map((m) => m.model_id));
75
+
76
+ // Re-query SKUs without model_name filter
77
+ const familyFilters: string[] = [];
78
+ if (params.direction) familyFilters.push(`direction=eq.${params.direction}`);
79
+ if (params.modality) familyFilters.push(`modality=ilike.*${params.modality}*`);
80
+ familyFilters.push("normalized_price=gt.0");
81
+
82
+ const allSkus = await queryTable<SkuIndex>("sku_index", familyFilters, {
83
+ select: "sku_id,model_id,vendor_name,model_name,modality,direction,normalized_price,normalized_price_unit,billing_method",
84
+ order: "normalized_price.asc",
85
+ limit: 200,
86
+ });
87
+
88
+ skus = allSkus.filter((s) => modelIds.has(s.model_id)).slice(0, params.limit);
89
+ }
90
+
91
+ if (skus.length === 0) {
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text" as const,
96
+ text: JSON.stringify({
97
+ tool: "compare_prices",
98
+ error: `No pricing data found for '${params.model_name || params.model_family}'.`,
99
+ }),
100
+ },
101
+ ],
102
+ };
103
+ }
104
+
105
+ const prices = skus
106
+ .map((s) => s.normalized_price)
107
+ .filter((p): p is number => p !== null && p > 0);
108
+
109
+ let results: unknown;
110
+
111
+ if (tier === "paid") {
112
+ results = {
113
+ comparisons: skus,
114
+ stats: {
115
+ total: skus.length,
116
+ vendors: [...new Set(skus.map((s) => s.vendor_name))].length,
117
+ cheapest: prices.length > 0 ? prices[0] : null,
118
+ most_expensive: prices.length > 0 ? prices[prices.length - 1] : null,
119
+ spread_ratio:
120
+ prices.length >= 2
121
+ ? +(prices[prices.length - 1] / prices[0]).toFixed(2)
122
+ : null,
123
+ },
124
+ };
125
+ } else {
126
+ results = {
127
+ summary: {
128
+ total_vendors: [...new Set(skus.map((s) => s.vendor_name))].length,
129
+ total_skus: skus.length,
130
+ price_range: {
131
+ min: prices.length > 0 ? prices[0] : null,
132
+ max: prices.length > 0 ? prices[prices.length - 1] : null,
133
+ },
134
+ spread_ratio:
135
+ prices.length >= 2
136
+ ? +(prices[prices.length - 1] / prices[0]).toFixed(2)
137
+ : null,
138
+ directions: [...new Set(skus.map((s) => s.direction))],
139
+ },
140
+ sample: gateResults(
141
+ skus.slice(0, 3) as unknown as Record<string, unknown>[],
142
+ "free"
143
+ ),
144
+ upgrade_message:
145
+ "Full vendor-by-vendor comparison requires ATOM MCP Pro ($49/mo). Visit https://a7om.com/mcp",
146
+ };
147
+ }
148
+
149
+ const content: { type: "text"; text: string }[] = [
150
+ {
151
+ type: "text" as const,
152
+ text: JSON.stringify(
153
+ {
154
+ tool: "compare_prices",
155
+ tier,
156
+ query: {
157
+ model_name: params.model_name,
158
+ model_family: params.model_family,
159
+ direction: params.direction,
160
+ modality: params.modality,
161
+ },
162
+ results,
163
+ },
164
+ null,
165
+ 2
166
+ ),
167
+ },
168
+ ];
169
+
170
+ if (tier === "free") {
171
+ content.push(freeTierNote("Full vendor-by-vendor price comparison"));
172
+ }
173
+
174
+ return { content };
175
+ }
@@ -0,0 +1,122 @@
1
+ // ============================================================
2
+ // Tool: get_index_benchmarks
3
+ // AIPI index family benchmarks — public market intelligence.
4
+ // ============================================================
5
+
6
+ import { z } from "zod";
7
+ import { queryTable } from "../supabase.js";
8
+ import type { Tier, IndexValues } from "../types.js";
9
+
10
+ export const getIndexBenchmarksSchema = {
11
+ index_code: z
12
+ .string()
13
+ .optional()
14
+ .describe(
15
+ "Filter by specific AIPI index code, e.g. 'AIPI-TXT-GLB', 'AIPI-IMG-GLB'. Omit to see all indexes."
16
+ ),
17
+ index_category: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ "Filter by index category, e.g. 'Text', 'Image', 'Audio', 'Video', 'Multimodal', 'Composite'"
22
+ ),
23
+ limit: z
24
+ .coerce.number()
25
+ .int()
26
+ .min(1)
27
+ .max(100)
28
+ .default(25)
29
+ .describe("Maximum results to return (default 25)"),
30
+ };
31
+
32
+ export async function handleGetIndexBenchmarks(
33
+ params: z.infer<z.ZodObject<typeof getIndexBenchmarksSchema>>,
34
+ tier: Tier
35
+ ) {
36
+ // Index benchmarks are fully public — no tier gating
37
+ const filters: string[] = [];
38
+ if (params.index_code && params.index_code.trim() !== "")
39
+ filters.push(`index_code=eq.${params.index_code.trim()}`);
40
+ if (
41
+ params.index_category &&
42
+ params.index_category.trim() !== "" &&
43
+ params.index_category !== "(any)"
44
+ )
45
+ filters.push(`index_category=ilike.*${params.index_category}*`);
46
+
47
+ const rows = await queryTable<IndexValues>("index_values", filters, {
48
+ order: "date.desc,index_code.asc",
49
+ limit: params.limit,
50
+ });
51
+
52
+ if (rows.length === 0) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text" as const,
57
+ text: JSON.stringify({
58
+ tool: "get_index_benchmarks",
59
+ error: params.index_code
60
+ ? `No index found for '${params.index_code}'. Omit index_code to see all available indexes.`
61
+ : "No index data available.",
62
+ }),
63
+ },
64
+ ],
65
+ };
66
+ }
67
+
68
+ // Extract unique index codes for summary
69
+ const indexCodes = [...new Set(rows.map((r) => r.index_code))];
70
+ const dates = [...new Set(rows.map((r) => r.date))].sort().reverse();
71
+
72
+ // Group by date for structured output
73
+ const byDate: Record<string, IndexValues[]> = {};
74
+ for (const row of rows) {
75
+ if (!byDate[row.date]) byDate[row.date] = [];
76
+ byDate[row.date].push(row);
77
+ }
78
+
79
+ // Format each entry
80
+ const formatted = rows.map((r) => ({
81
+ index_code: r.index_code,
82
+ index_category: r.index_category,
83
+ description: r.index_description,
84
+ date: r.date,
85
+ unit: r.unit,
86
+ input_price: r.input_price,
87
+ cached_price: r.cached_price,
88
+ output_price: r.output_price,
89
+ sku_count: r.sku_count,
90
+ }));
91
+
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text" as const,
96
+ text: JSON.stringify(
97
+ {
98
+ tool: "get_index_benchmarks",
99
+ tier,
100
+ description:
101
+ "AIPI (ATOM Inference Price Index) — chained matched-model price benchmarks for AI inference.",
102
+ summary: {
103
+ total_indexes: indexCodes.length,
104
+ indexes_available: indexCodes,
105
+ date_range: {
106
+ latest: dates[0],
107
+ earliest: dates[dates.length - 1],
108
+ total_periods: dates.length,
109
+ },
110
+ },
111
+ benchmarks: formatted,
112
+ methodology:
113
+ "Chained matched-model index. Only SKUs present in consecutive periods are compared, eliminating composition bias. See https://a7om.com/methodology",
114
+ source: "https://a7om.com",
115
+ },
116
+ null,
117
+ 2
118
+ ),
119
+ },
120
+ ],
121
+ };
122
+ }