mcp-server-auction 0.2.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/build/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerTools } from "./tools.js";
5
+ const server = new McpServer({
6
+ name: "server-auction",
7
+ version: "0.2.0",
8
+ });
9
+ registerTools(server);
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ console.error("server-auction MCP server running on stdio");
@@ -0,0 +1,257 @@
1
+ import { getCpuVendor } from "../utils.js";
2
+ // ---------- Cache ----------
3
+ const API_URL = "https://www.hetzner.com/_resources/app/data/app/live_data_sb_EUR.json";
4
+ const CACHE_TTL_MS = 5 * 60 * 1000;
5
+ let cache = null;
6
+ async function fetchRawAuctions() {
7
+ if (cache && Date.now() - cache.timestamp < CACHE_TTL_MS) {
8
+ return cache.data;
9
+ }
10
+ const res = await fetch(API_URL);
11
+ if (!res.ok) {
12
+ throw new Error(`Hetzner API returned ${res.status}: ${res.statusText}`);
13
+ }
14
+ const json = (await res.json());
15
+ cache = { data: json.server, timestamp: Date.now() };
16
+ return json.server;
17
+ }
18
+ // ---------- Hetzner helpers ----------
19
+ function hasGpu(server) {
20
+ return server.specials.some((s) => s.toUpperCase() === "GPU");
21
+ }
22
+ function getGpuModel(server) {
23
+ for (const desc of server.description) {
24
+ const match = desc.match(/GPU\s*-\s*(.+)/i);
25
+ if (match)
26
+ return match[1].trim();
27
+ }
28
+ return null;
29
+ }
30
+ function getDatacenterRegion(dc) {
31
+ const match = dc.match(/^([A-Z]+)/i);
32
+ return match ? match[1].toUpperCase() : dc;
33
+ }
34
+ function getCountry(dc) {
35
+ const region = getDatacenterRegion(dc);
36
+ if (region === "FSN" || region === "NBG")
37
+ return "DE";
38
+ if (region === "HEL")
39
+ return "FI";
40
+ return "DE";
41
+ }
42
+ function buildDisks(d) {
43
+ const disks = [];
44
+ const addGroup = (sizes, type) => {
45
+ const groups = new Map();
46
+ for (const size of sizes) {
47
+ groups.set(size, (groups.get(size) ?? 0) + 1);
48
+ }
49
+ for (const [size, count] of groups) {
50
+ disks.push({ count, size_gb: size, type });
51
+ }
52
+ };
53
+ addGroup(d.nvme, "nvme");
54
+ addGroup(d.sata, "sata");
55
+ addGroup(d.hdd, "hdd");
56
+ addGroup(d.general, "unknown");
57
+ return disks;
58
+ }
59
+ function normalizeServer(server, cores) {
60
+ const disks = buildDisks(server.serverDiskData);
61
+ return {
62
+ id: String(server.id),
63
+ provider: "hetzner",
64
+ url: `https://www.hetzner.com/sb?id=${server.id}`,
65
+ name: server.cpu,
66
+ cpu: server.cpu,
67
+ cpu_count: server.cpu_count,
68
+ cpu_cores: cores,
69
+ cpu_vendor: getCpuVendor(server.cpu),
70
+ ram_gb: server.ram_size,
71
+ is_ecc: server.is_ecc,
72
+ disks,
73
+ disk_total_gb: disks.reduce((sum, d) => sum + d.size_gb * d.count, 0),
74
+ disk_count: disks.reduce((sum, d) => sum + d.count, 0),
75
+ bandwidth_mbps: server.bandwidth,
76
+ datacenter: server.datacenter,
77
+ datacenter_region: getDatacenterRegion(server.datacenter),
78
+ country: getCountry(server.datacenter),
79
+ price_monthly_eur: server.price,
80
+ price_setup_eur: server.setup_price,
81
+ price_hourly_eur: server.hourly_price,
82
+ gpu: hasGpu(server),
83
+ gpu_model: getGpuModel(server),
84
+ is_highio: server.is_highio,
85
+ fixed_price: server.fixed_price,
86
+ next_reduce_timestamp: server.fixed_price
87
+ ? null
88
+ : server.next_reduce_timestamp,
89
+ availability: null,
90
+ specials: server.specials,
91
+ };
92
+ }
93
+ // ---------- CPU core resolution ----------
94
+ // Hetzner doesn't include core counts in its API, so we resolve them
95
+ // via a static lookup table + GitHub CSV fallback.
96
+ const KNOWN_CORES = {
97
+ "AMD EPYC 7401P": 24,
98
+ "AMD EPYC 7502": 32,
99
+ "AMD EPYC 7502P": 32,
100
+ "AMD Ryzen 5 3600": 6,
101
+ "AMD Ryzen 7 1700X": 8,
102
+ "AMD Ryzen 7 3700X": 8,
103
+ "AMD Ryzen 7 7700": 8,
104
+ "AMD Ryzen 7 PRO 1700X": 8,
105
+ "AMD Ryzen 9 3900": 12,
106
+ "AMD Ryzen 9 5950X": 16,
107
+ "AMD Ryzen Threadripper 2950X": 16,
108
+ "Intel Core i5-12500": 6,
109
+ "Intel Core i7-6700": 4,
110
+ "Intel Core i7-7700": 4,
111
+ "Intel Core i7-8700": 6,
112
+ "Intel Core i9-9900K": 8,
113
+ "Intel Core i9-12900K": 16,
114
+ "Intel Core i9-13900": 24,
115
+ "Intel Xeon E3-1270V3": 4,
116
+ "Intel Xeon E3-1271V3": 4,
117
+ "Intel Xeon E3-1275v5": 4,
118
+ "Intel Xeon E3-1275V6": 4,
119
+ "Intel Xeon E5-1650V3": 6,
120
+ "Intel XEON E-2176G": 6,
121
+ "Intel XEON E-2276G": 6,
122
+ "Intel Xeon W-2145": 8,
123
+ "Intel Xeon W-2245": 8,
124
+ "Intel Xeon W-2295": 18,
125
+ "Intel Xeon Gold 5412U": 24,
126
+ };
127
+ function normaliseCpuName(name) {
128
+ return name
129
+ .replace(/[®™]/g, "")
130
+ .replace(/\s+/g, " ")
131
+ .trim()
132
+ .toLowerCase();
133
+ }
134
+ function extractModelToken(name) {
135
+ const cleaned = normaliseCpuName(name);
136
+ const m = cleaned.match(/(?:i[3579]-\d{4,5}[a-z]*|e[35]-\d{4}\s*v?\d*|e-\d{4}[a-z]*|w-\d{4,5}[a-z]*|gold \d{4}[a-z]*|epyc \d{4}[a-z]*|ryzen \d+ (?:pro )?\d{4}[a-z]*|threadripper \d{4,5}[a-z]*)/);
137
+ return m ? m[0] : null;
138
+ }
139
+ function parseCsvRow(line) {
140
+ const fields = [];
141
+ let current = "";
142
+ let inQuotes = false;
143
+ for (let i = 0; i < line.length; i++) {
144
+ const ch = line[i];
145
+ if (inQuotes) {
146
+ if (ch === '"') {
147
+ if (i + 1 < line.length && line[i + 1] === '"') {
148
+ current += '"';
149
+ i++;
150
+ }
151
+ else {
152
+ inQuotes = false;
153
+ }
154
+ }
155
+ else {
156
+ current += ch;
157
+ }
158
+ }
159
+ else if (ch === '"') {
160
+ inQuotes = true;
161
+ }
162
+ else if (ch === ",") {
163
+ fields.push(current);
164
+ current = "";
165
+ }
166
+ else {
167
+ current += ch;
168
+ }
169
+ }
170
+ fields.push(current);
171
+ return fields;
172
+ }
173
+ const INTEL_CSV_URL = "https://raw.githubusercontent.com/felixsteinke/cpu-spec-dataset/main/dataset/intel-cpus.csv";
174
+ const AMD_CSV_URL = "https://raw.githubusercontent.com/felixsteinke/cpu-spec-dataset/main/dataset/amd-cpus.csv";
175
+ const CSV_CACHE_TTL_MS = 5 * 60 * 1000;
176
+ let csvCache = null;
177
+ async function fetchCsvCoreMap() {
178
+ if (csvCache && Date.now() - csvCache.timestamp < CSV_CACHE_TTL_MS) {
179
+ return csvCache.map;
180
+ }
181
+ const map = new Map();
182
+ try {
183
+ const [intelRes, amdRes] = await Promise.all([
184
+ fetch(INTEL_CSV_URL),
185
+ fetch(AMD_CSV_URL),
186
+ ]);
187
+ for (const { res, nameCol, coreCol } of [
188
+ { res: intelRes, nameCol: "CpuName", coreCol: "CoreCount" },
189
+ { res: amdRes, nameCol: "Model", coreCol: "# of CPU Cores" },
190
+ ]) {
191
+ if (!res.ok)
192
+ continue;
193
+ const text = await res.text();
194
+ const lines = text.split("\n");
195
+ const header = parseCsvRow(lines[0]);
196
+ const nameIdx = header.indexOf(nameCol);
197
+ const coreIdx = header.indexOf(coreCol);
198
+ if (nameIdx < 0 || coreIdx < 0)
199
+ continue;
200
+ for (let i = 1; i < lines.length; i++) {
201
+ if (!lines[i].trim())
202
+ continue;
203
+ const row = parseCsvRow(lines[i]);
204
+ const cores = parseInt(row[coreIdx], 10);
205
+ if (!isNaN(cores) && row[nameIdx]) {
206
+ const token = extractModelToken(row[nameIdx]);
207
+ if (token && !map.has(token))
208
+ map.set(token, cores);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ catch {
214
+ // Network failure — return whatever we have.
215
+ }
216
+ csvCache = { map, timestamp: Date.now() };
217
+ return map;
218
+ }
219
+ async function resolveCpuCores(cpuName) {
220
+ if (cpuName in KNOWN_CORES)
221
+ return KNOWN_CORES[cpuName];
222
+ const norm = normaliseCpuName(cpuName);
223
+ for (const [key, cores] of Object.entries(KNOWN_CORES)) {
224
+ if (normaliseCpuName(key) === norm)
225
+ return cores;
226
+ }
227
+ const token = extractModelToken(cpuName);
228
+ if (token) {
229
+ const csvMap = await fetchCsvCoreMap();
230
+ const csvCores = csvMap.get(token);
231
+ if (csvCores !== undefined)
232
+ return csvCores;
233
+ }
234
+ return null;
235
+ }
236
+ async function resolveCpuCoresBulk(cpuNames) {
237
+ const unique = [...new Set(cpuNames)];
238
+ const result = new Map();
239
+ for (const name of unique) {
240
+ result.set(name, await resolveCpuCores(name));
241
+ }
242
+ return result;
243
+ }
244
+ // ---------- Provider ----------
245
+ export const hetznerProvider = {
246
+ name: "hetzner",
247
+ displayName: "Hetzner",
248
+ async fetchListings() {
249
+ const servers = await fetchRawAuctions();
250
+ const coreMap = await resolveCpuCoresBulk(servers.map((s) => s.cpu));
251
+ return servers.map((s) => {
252
+ const perSocket = coreMap.get(s.cpu) ?? null;
253
+ const cores = perSocket !== null ? perSocket * s.cpu_count : null;
254
+ return normalizeServer(s, cores);
255
+ });
256
+ },
257
+ };
@@ -0,0 +1,202 @@
1
+ import { getCpuVendor } from "../utils.js";
2
+ // ---------- Cache ----------
3
+ const CATALOG_URL = "https://eu.api.ovh.com/v1/order/catalog/public/eco?ovhSubsidiary=DE";
4
+ const AVAILABILITY_URL = "https://eu.api.ovh.com/v1/dedicated/server/datacenter/availabilities";
5
+ const CACHE_TTL_MS = 5 * 60 * 1000;
6
+ let catalogCache = null;
7
+ let availabilityCache = null;
8
+ async function fetchCatalog() {
9
+ if (catalogCache && Date.now() - catalogCache.timestamp < CACHE_TTL_MS) {
10
+ return catalogCache.data;
11
+ }
12
+ const res = await fetch(CATALOG_URL);
13
+ if (!res.ok) {
14
+ throw new Error(`OVH Catalog API returned ${res.status}: ${res.statusText}`);
15
+ }
16
+ const data = (await res.json());
17
+ catalogCache = { data, timestamp: Date.now() };
18
+ return data;
19
+ }
20
+ async function fetchAvailability() {
21
+ if (availabilityCache &&
22
+ Date.now() - availabilityCache.timestamp < CACHE_TTL_MS) {
23
+ return availabilityCache.data;
24
+ }
25
+ const res = await fetch(AVAILABILITY_URL);
26
+ if (!res.ok) {
27
+ throw new Error(`OVH Availability API returned ${res.status}: ${res.statusText}`);
28
+ }
29
+ const data = (await res.json());
30
+ availabilityCache = { data, timestamp: Date.now() };
31
+ return data;
32
+ }
33
+ // ---------- DC mapping ----------
34
+ const DC_COUNTRY = {
35
+ fra: "DE",
36
+ gra: "FR",
37
+ rbx: "FR",
38
+ sbg: "FR",
39
+ lon: "GB",
40
+ waw: "PL",
41
+ bhs: "CA",
42
+ };
43
+ function dcToCountry(dc) {
44
+ const prefix = dc.replace(/\d+$/, "").toLowerCase();
45
+ return DC_COUNTRY[prefix] ?? "FR";
46
+ }
47
+ function dcToRegion(dc) {
48
+ return dc.replace(/\d+$/, "").toUpperCase();
49
+ }
50
+ function parseMemoryCode(code) {
51
+ // e.g. "ram-32g-ecc-3200", "ram-64g-noecc-2400"
52
+ const match = code.match(/ram-(\d+)g/i);
53
+ if (!match)
54
+ return null;
55
+ return {
56
+ size_gb: parseInt(match[1], 10),
57
+ is_ecc: code.toLowerCase().includes("ecc") && !code.toLowerCase().includes("noecc"),
58
+ };
59
+ }
60
+ function parseStorageCode(code) {
61
+ // e.g. "softraid-2x512nvme", "softraid-2x2000sa", "softraid-4x960ssd"
62
+ // Also handle "raid" variants: "raid1-2x512nvme"
63
+ const match = code.match(/(\d+)x(\d+)(nvme|ssd|sa|hdd)/i);
64
+ if (!match)
65
+ return [];
66
+ const count = parseInt(match[1], 10);
67
+ const size = parseInt(match[2], 10);
68
+ const rawType = match[3].toLowerCase();
69
+ const type = rawType === "nvme" ? "nvme"
70
+ : rawType === "ssd" ? "sata"
71
+ : rawType === "sa" ? "hdd"
72
+ : rawType === "hdd" ? "hdd"
73
+ : "unknown";
74
+ return [{ count, size_gb: size, type }];
75
+ }
76
+ // ---------- Price normalization ----------
77
+ function extractMonthlyPrice(plan) {
78
+ // Look for monthly renew pricing
79
+ const renew = plan.pricings.find((p) => p.capacities.includes("renew") && (p.interval === 1 || p.interval === undefined));
80
+ if (!renew)
81
+ return 0;
82
+ // OVH catalog prices are in cents (hundredths of EUR)
83
+ const priceEur = renew.price / 100_000_000;
84
+ // Sanity check: if price seems too low after division, try cents
85
+ if (priceEur < 1 && renew.price > 100)
86
+ return renew.price / 100;
87
+ return priceEur;
88
+ }
89
+ function extractSetupPrice(plan) {
90
+ const install = plan.pricings.find((p) => p.capacities.includes("installation"));
91
+ if (!install)
92
+ return 0;
93
+ const priceEur = install.price / 100_000_000;
94
+ if (priceEur < 0.01 && install.price > 100)
95
+ return install.price / 100;
96
+ return priceEur;
97
+ }
98
+ // ---------- Normalization ----------
99
+ function buildListing(entry, cpuInfo, pricing, planName, availableDcs) {
100
+ const mem = parseMemoryCode(entry.memory);
101
+ const disks = parseStorageCode(entry.storage);
102
+ const cpuName = cpuInfo
103
+ ? `${cpuInfo.brand ?? ""} ${cpuInfo.model ?? ""}`.trim()
104
+ : planName;
105
+ const cpuCores = cpuInfo?.cores ?? null;
106
+ const firstDc = availableDcs[0];
107
+ const dcList = availableDcs.map((d) => dcToRegion(d.datacenter));
108
+ const uniqueDcs = [...new Set(dcList)];
109
+ // Best availability (prefer "available" over time-based like "1H", "24H", etc.)
110
+ const bestAvail = availableDcs.find((d) => d.availability === "available")?.availability ??
111
+ availableDcs[0]?.availability ??
112
+ "unknown";
113
+ return {
114
+ id: entry.fqn,
115
+ provider: "ovh",
116
+ url: "https://eco.ovhcloud.com/en/",
117
+ name: planName,
118
+ cpu: cpuName,
119
+ cpu_count: 1,
120
+ cpu_cores: cpuCores,
121
+ cpu_vendor: getCpuVendor(cpuName),
122
+ ram_gb: mem?.size_gb ?? 0,
123
+ is_ecc: mem?.is_ecc ?? false,
124
+ disks,
125
+ disk_total_gb: disks.reduce((sum, d) => sum + d.size_gb * d.count, 0),
126
+ disk_count: disks.reduce((sum, d) => sum + d.count, 0),
127
+ bandwidth_mbps: 1000, // OVH ECO default: 1 Gbit
128
+ datacenter: uniqueDcs.join(", "),
129
+ datacenter_region: uniqueDcs[0] ?? "",
130
+ country: firstDc ? dcToCountry(firstDc.datacenter) : "FR",
131
+ price_monthly_eur: pricing.monthly,
132
+ price_setup_eur: pricing.setup,
133
+ price_hourly_eur: null,
134
+ gpu: entry.gpu !== undefined && entry.gpu !== null && entry.gpu !== "",
135
+ gpu_model: entry.gpu || null,
136
+ is_highio: false,
137
+ fixed_price: true,
138
+ next_reduce_timestamp: null,
139
+ availability: bestAvail,
140
+ specials: [],
141
+ };
142
+ }
143
+ // ---------- Provider ----------
144
+ export const ovhProvider = {
145
+ name: "ovh",
146
+ displayName: "OVH",
147
+ async fetchListings() {
148
+ const [catalog, availability] = await Promise.all([
149
+ fetchCatalog(),
150
+ fetchAvailability(),
151
+ ]);
152
+ // Build CPU info lookup: planCode → CPU specs (from first matching product)
153
+ const cpuByPlan = new Map();
154
+ for (const product of catalog.products) {
155
+ const planCode = product.name.split(".")[0];
156
+ if (!cpuByPlan.has(planCode) &&
157
+ product.blobs?.technical?.server?.cpu) {
158
+ cpuByPlan.set(planCode, product.blobs.technical.server.cpu);
159
+ }
160
+ }
161
+ // Build pricing lookup: planCode → { monthly, setup }
162
+ const pricingByPlan = new Map();
163
+ for (const plan of catalog.plans) {
164
+ if (!pricingByPlan.has(plan.planCode)) {
165
+ pricingByPlan.set(plan.planCode, {
166
+ monthly: extractMonthlyPrice(plan),
167
+ setup: extractSetupPrice(plan),
168
+ name: plan.invoiceName ?? plan.planCode,
169
+ });
170
+ }
171
+ }
172
+ // Group availability entries by FQN (deduplicate)
173
+ const byFqn = new Map();
174
+ for (const entry of availability) {
175
+ const availDcs = entry.datacenters.filter((d) => d.availability !== "unavailable");
176
+ if (availDcs.length === 0)
177
+ continue;
178
+ const existing = byFqn.get(entry.fqn);
179
+ if (existing) {
180
+ // Merge DCs
181
+ for (const dc of availDcs) {
182
+ if (!existing.dcs.some((d) => d.datacenter === dc.datacenter)) {
183
+ existing.dcs.push(dc);
184
+ }
185
+ }
186
+ }
187
+ else {
188
+ byFqn.set(entry.fqn, { entry, dcs: availDcs });
189
+ }
190
+ }
191
+ // Build listings
192
+ const listings = [];
193
+ for (const { entry, dcs } of byFqn.values()) {
194
+ const cpuInfo = cpuByPlan.get(entry.planCode) ?? null;
195
+ const planInfo = pricingByPlan.get(entry.planCode);
196
+ if (!planInfo)
197
+ continue; // Skip entries without pricing info
198
+ listings.push(buildListing(entry, cpuInfo, { monthly: planInfo.monthly, setup: planInfo.setup }, planInfo.name, dcs));
199
+ }
200
+ return listings;
201
+ },
202
+ };
@@ -0,0 +1,17 @@
1
+ import { hetznerProvider } from "./hetzner.js";
2
+ import { ovhProvider } from "./ovh.js";
3
+ const providers = [hetznerProvider, ovhProvider];
4
+ export function getProviderNames() {
5
+ return providers.map((p) => p.name);
6
+ }
7
+ export async function getAllListings(providerFilter) {
8
+ const selected = providerFilter && providerFilter !== "all"
9
+ ? providers.filter((p) => p.name === providerFilter)
10
+ : providers;
11
+ const results = await Promise.all(selected.map((p) => p.fetchListings().catch(() => [])));
12
+ return results.flat();
13
+ }
14
+ export async function getListingById(id, providerFilter) {
15
+ const listings = await getAllListings(providerFilter);
16
+ return listings.find((l) => l.id === id);
17
+ }
package/build/tools.js ADDED
@@ -0,0 +1,349 @@
1
+ import { z } from "zod";
2
+ import { getDiskType, formatDiskSummary, formatBandwidth, formatNextReduce, } from "./utils.js";
3
+ import { getAllListings, getListingById } from "./providers/registry.js";
4
+ const providerEnum = z
5
+ .enum(["hetzner", "ovh", "all"])
6
+ .optional()
7
+ .describe("Provider filter: hetzner, ovh, or all (default: all)");
8
+ export function registerTools(server) {
9
+ server.tool("search_auctions", "Search and filter dedicated server auction listings by price, RAM, disk, CPU, datacenter, GPU, bandwidth, and more", {
10
+ provider: providerEnum,
11
+ max_price: z.number().optional().describe("Maximum monthly price in EUR"),
12
+ min_ram: z.number().optional().describe("Minimum RAM in GB"),
13
+ min_disk_size: z
14
+ .number()
15
+ .optional()
16
+ .describe("Minimum total disk capacity in GB"),
17
+ min_disk_count: z
18
+ .number()
19
+ .optional()
20
+ .describe("Minimum number of disks"),
21
+ disk_type: z
22
+ .enum(["ssd", "nvme", "hdd", "any"])
23
+ .optional()
24
+ .describe("Filter by disk type (ssd matches both SATA SSD and NVMe)"),
25
+ cpu_vendor: z
26
+ .enum(["intel", "amd", "any"])
27
+ .optional()
28
+ .describe("CPU vendor filter"),
29
+ cpu_search: z
30
+ .string()
31
+ .optional()
32
+ .describe("Free-text search within CPU model name (e.g. 'EPYC 7502', 'Xeon E5', 'Ryzen 9')"),
33
+ min_cpu_count: z
34
+ .number()
35
+ .optional()
36
+ .describe("Minimum CPU/socket count"),
37
+ min_cores: z
38
+ .number()
39
+ .optional()
40
+ .describe("Minimum total CPU cores (resolved from model name, accounts for multi-socket)"),
41
+ ecc: z.boolean().optional().describe("Require ECC RAM"),
42
+ datacenter: z
43
+ .string()
44
+ .optional()
45
+ .describe("Datacenter filter (e.g. FSN, NBG, HEL, FRA, GRA, or FSN1-DC1)"),
46
+ gpu: z.boolean().optional().describe("Require GPU"),
47
+ min_bandwidth: z
48
+ .number()
49
+ .optional()
50
+ .describe("Minimum bandwidth in Mbit (e.g. 1000 = 1 Gbit)"),
51
+ fixed_price: z
52
+ .boolean()
53
+ .optional()
54
+ .describe("Filter by pricing type: true = fixed price only, false = auction only"),
55
+ max_setup_price: z
56
+ .number()
57
+ .optional()
58
+ .describe("Maximum setup price in EUR (use 0 for no setup fee)"),
59
+ highio: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Require high I/O hardware"),
63
+ sort_by: z
64
+ .enum(["price", "ram", "disk_size", "cpu", "cores"])
65
+ .optional()
66
+ .describe("Sort field (default: price)"),
67
+ limit: z
68
+ .number()
69
+ .min(1)
70
+ .max(50)
71
+ .optional()
72
+ .describe("Max results (default: 10)"),
73
+ }, async (params) => {
74
+ const listings = await getAllListings(params.provider);
75
+ let filtered = listings.filter((s) => {
76
+ if (params.max_price !== undefined &&
77
+ s.price_monthly_eur > params.max_price)
78
+ return false;
79
+ if (params.min_ram !== undefined && s.ram_gb < params.min_ram)
80
+ return false;
81
+ if (params.min_disk_size !== undefined &&
82
+ s.disk_total_gb < params.min_disk_size)
83
+ return false;
84
+ if (params.min_disk_count !== undefined &&
85
+ s.disk_count < params.min_disk_count)
86
+ return false;
87
+ if (params.disk_type !== undefined && params.disk_type !== "any") {
88
+ const dt = getDiskType(s);
89
+ if (params.disk_type === "ssd") {
90
+ if (dt !== "nvme" && dt !== "sata")
91
+ return false;
92
+ }
93
+ else if (params.disk_type === "nvme") {
94
+ if (dt !== "nvme")
95
+ return false;
96
+ }
97
+ else if (params.disk_type === "hdd") {
98
+ if (dt !== "hdd")
99
+ return false;
100
+ }
101
+ }
102
+ if (params.cpu_vendor !== undefined &&
103
+ params.cpu_vendor !== "any") {
104
+ if (s.cpu_vendor !== params.cpu_vendor)
105
+ return false;
106
+ }
107
+ if (params.cpu_search !== undefined) {
108
+ if (!s.cpu.toLowerCase().includes(params.cpu_search.toLowerCase()))
109
+ return false;
110
+ }
111
+ if (params.min_cpu_count !== undefined &&
112
+ s.cpu_count < params.min_cpu_count)
113
+ return false;
114
+ if (params.min_cores !== undefined) {
115
+ if (s.cpu_cores === null || s.cpu_cores < params.min_cores)
116
+ return false;
117
+ }
118
+ if (params.ecc === true && !s.is_ecc)
119
+ return false;
120
+ if (params.datacenter !== undefined) {
121
+ const dcUpper = params.datacenter.toUpperCase();
122
+ // Support comma-separated datacenter lists (e.g. OVH multi-DC)
123
+ const dcs = s.datacenter
124
+ .toUpperCase()
125
+ .split(",")
126
+ .map((d) => d.trim());
127
+ if (!dcs.some((d) => d.startsWith(dcUpper)))
128
+ return false;
129
+ }
130
+ if (params.gpu === true && !s.gpu)
131
+ return false;
132
+ if (params.min_bandwidth !== undefined &&
133
+ s.bandwidth_mbps < params.min_bandwidth)
134
+ return false;
135
+ if (params.fixed_price !== undefined &&
136
+ s.fixed_price !== params.fixed_price)
137
+ return false;
138
+ if (params.max_setup_price !== undefined &&
139
+ s.price_setup_eur > params.max_setup_price)
140
+ return false;
141
+ if (params.highio === true && !s.is_highio)
142
+ return false;
143
+ return true;
144
+ });
145
+ const sortBy = params.sort_by ?? "price";
146
+ filtered.sort((a, b) => {
147
+ switch (sortBy) {
148
+ case "price":
149
+ return a.price_monthly_eur - b.price_monthly_eur;
150
+ case "ram":
151
+ return b.ram_gb - a.ram_gb;
152
+ case "disk_size":
153
+ return b.disk_total_gb - a.disk_total_gb;
154
+ case "cpu":
155
+ return b.cpu_count - a.cpu_count;
156
+ case "cores":
157
+ return (b.cpu_cores ?? 0) - (a.cpu_cores ?? 0);
158
+ default:
159
+ return a.price_monthly_eur - b.price_monthly_eur;
160
+ }
161
+ });
162
+ const limit = params.limit ?? 10;
163
+ const results = filtered.slice(0, limit);
164
+ if (results.length === 0) {
165
+ return {
166
+ content: [
167
+ {
168
+ type: "text",
169
+ text: "No servers found matching your criteria.",
170
+ },
171
+ ],
172
+ };
173
+ }
174
+ const lines = [
175
+ `Found ${filtered.length} servers matching your criteria (showing top ${results.length}):\n`,
176
+ ];
177
+ for (let i = 0; i < results.length; i++) {
178
+ lines.push(formatServerLine(results[i], i + 1));
179
+ }
180
+ return { content: [{ type: "text", text: lines.join("\n") }] };
181
+ });
182
+ server.tool("get_auction_stats", "Get aggregate statistics about current server auction listings", {
183
+ provider: providerEnum,
184
+ }, async (params) => {
185
+ const listings = await getAllListings(params.provider);
186
+ const prices = listings
187
+ .map((s) => s.price_monthly_eur)
188
+ .sort((a, b) => a - b);
189
+ if (prices.length === 0) {
190
+ return {
191
+ content: [{ type: "text", text: "No listings available." }],
192
+ };
193
+ }
194
+ const avg = prices.reduce((a, b) => a + b, 0) / prices.length;
195
+ const median = prices.length % 2 === 0
196
+ ? (prices[prices.length / 2 - 1] + prices[prices.length / 2]) / 2
197
+ : prices[Math.floor(prices.length / 2)];
198
+ // Provider breakdown
199
+ const providerCounts = new Map();
200
+ for (const s of listings) {
201
+ providerCounts.set(s.provider, (providerCounts.get(s.provider) ?? 0) + 1);
202
+ }
203
+ const providerLines = [...providerCounts.entries()]
204
+ .sort((a, b) => b[1] - a[1])
205
+ .map(([p, n]) => ` ${p.charAt(0).toUpperCase() + p.slice(1)}: ${n} servers`);
206
+ // CPU vendor breakdown
207
+ const intelListings = listings.filter((s) => s.cpu_vendor === "intel");
208
+ const amdListings = listings.filter((s) => s.cpu_vendor === "amd");
209
+ const intelAvg = intelListings.length > 0
210
+ ? intelListings.reduce((a, s) => a + s.price_monthly_eur, 0) /
211
+ intelListings.length
212
+ : 0;
213
+ const amdAvg = amdListings.length > 0
214
+ ? amdListings.reduce((a, s) => a + s.price_monthly_eur, 0) /
215
+ amdListings.length
216
+ : 0;
217
+ // Datacenter/region breakdown
218
+ const dcMap = new Map();
219
+ for (const s of listings) {
220
+ const regions = s.datacenter
221
+ .split(",")
222
+ .map((d) => d.trim())
223
+ .filter(Boolean);
224
+ for (const region of regions) {
225
+ const key = `${s.provider.charAt(0).toUpperCase() + s.provider.slice(1)} ${region}`;
226
+ dcMap.set(key, (dcMap.get(key) ?? 0) + 1);
227
+ }
228
+ }
229
+ const dcLines = [...dcMap.entries()]
230
+ .sort((a, b) => b[1] - a[1])
231
+ .map(([dc, n]) => ` ${dc}: ${n} servers`);
232
+ // RAM tier distribution
233
+ const ramTiers = new Map();
234
+ for (const s of listings) {
235
+ const tier = s.ram_gb >= 256 ? "256+ GB"
236
+ : s.ram_gb >= 128 ? "128-255 GB"
237
+ : s.ram_gb >= 64 ? "64-127 GB"
238
+ : s.ram_gb >= 32 ? "32-63 GB"
239
+ : "<32 GB";
240
+ ramTiers.set(tier, (ramTiers.get(tier) ?? 0) + 1);
241
+ }
242
+ const ramOrder = [
243
+ "<32 GB",
244
+ "32-63 GB",
245
+ "64-127 GB",
246
+ "128-255 GB",
247
+ "256+ GB",
248
+ ];
249
+ const ramLines = ramOrder
250
+ .filter((t) => ramTiers.has(t))
251
+ .map((t) => ` ${t}: ${ramTiers.get(t)} servers`);
252
+ const gpuCount = listings.filter((s) => s.gpu).length;
253
+ const eccCount = listings.filter((s) => s.is_ecc).length;
254
+ const text = [
255
+ `Server Auction — Live Statistics`,
256
+ `=================================`,
257
+ ``,
258
+ `Total listings: ${listings.length}`,
259
+ ``,
260
+ `Provider:`,
261
+ ...providerLines,
262
+ ``,
263
+ `Price (EUR/month):`,
264
+ ` Min: \u20AC${prices[0].toFixed(2)} Max: \u20AC${prices[prices.length - 1].toFixed(2)} Avg: \u20AC${avg.toFixed(2)} Median: \u20AC${median.toFixed(2)}`,
265
+ ``,
266
+ `CPU Vendor:`,
267
+ ` Intel: ${intelListings.length} servers (avg \u20AC${intelAvg.toFixed(2)}/mo)`,
268
+ ` AMD: ${amdListings.length} servers (avg \u20AC${amdAvg.toFixed(2)}/mo)`,
269
+ ``,
270
+ `Datacenter Region:`,
271
+ ...dcLines,
272
+ ``,
273
+ `RAM Tiers:`,
274
+ ...ramLines,
275
+ ``,
276
+ `GPU servers: ${gpuCount}`,
277
+ `ECC servers: ${eccCount}`,
278
+ ];
279
+ return { content: [{ type: "text", text: text.join("\n") }] };
280
+ });
281
+ server.tool("get_auction_server", "Get full details of a specific auction server by its ID", {
282
+ server_id: z.string().describe("The server ID (numeric for Hetzner, FQN for OVH)"),
283
+ provider: providerEnum,
284
+ }, async (params) => {
285
+ const listing = await getListingById(params.server_id, params.provider);
286
+ if (!listing) {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Server "${params.server_id}" not found in current listings.`,
292
+ },
293
+ ],
294
+ };
295
+ }
296
+ const providerTag = listing.provider.toUpperCase();
297
+ const coreStr = listing.cpu_cores !== null ? `, ${listing.cpu_cores} cores` : "";
298
+ const hourlyStr = listing.price_hourly_eur !== null
299
+ ? ` (\u20AC${listing.price_hourly_eur.toFixed(4)}/hr)`
300
+ : "";
301
+ const setupStr = listing.price_setup_eur > 0
302
+ ? ` + \u20AC${listing.price_setup_eur.toFixed(2)} setup`
303
+ : "";
304
+ const lines = [
305
+ `[${providerTag}] ${listing.id} — Full Details`,
306
+ `${"=".repeat(50)}`,
307
+ ``,
308
+ `CPU: ${listing.cpu} (${listing.cpu_count} socket${listing.cpu_count > 1 ? "s" : ""}${coreStr})`,
309
+ `RAM: ${listing.ram_gb} GB${listing.is_ecc ? " (ECC)" : ""}`,
310
+ `Disks: ${formatDiskSummary(listing)}`,
311
+ ` Total: ${listing.disk_total_gb} GB across ${listing.disk_count} drive${listing.disk_count > 1 ? "s" : ""}`,
312
+ `Datacenter: ${listing.datacenter} (${listing.country})`,
313
+ `Bandwidth: ${formatBandwidth(listing.bandwidth_mbps)}`,
314
+ ...(listing.gpu
315
+ ? [`GPU: ${listing.gpu_model ?? "Yes"}`]
316
+ : []),
317
+ ``,
318
+ `Price: \u20AC${listing.price_monthly_eur.toFixed(2)}/mo${hourlyStr}${setupStr}`,
319
+ `Type: ${formatNextReduce(listing)}`,
320
+ ...(listing.is_highio ? [`High I/O: Yes`] : []),
321
+ ...(listing.availability
322
+ ? [`Availability: ${listing.availability}`]
323
+ : []),
324
+ ...(listing.specials.length > 0
325
+ ? [`Specials: ${listing.specials.join(", ")}`]
326
+ : []),
327
+ ``,
328
+ `View: ${listing.url}`,
329
+ ];
330
+ return { content: [{ type: "text", text: lines.join("\n") }] };
331
+ });
332
+ }
333
+ function formatServerLine(s, index) {
334
+ const providerTag = `[${s.provider.toUpperCase()}]`;
335
+ const coreStr = s.cpu_cores !== null ? ` (${s.cpu_cores} cores)` : "";
336
+ const ecc = s.is_ecc ? " (ECC)" : "";
337
+ const gpu = s.gpu ? ` | GPU: ${s.gpu_model ?? "Yes"}` : "";
338
+ const disk = formatDiskSummary(s);
339
+ const priceInfo = formatNextReduce(s);
340
+ const hourly = s.price_hourly_eur !== null
341
+ ? ` (\u20AC${s.price_hourly_eur.toFixed(4)}/hr)`
342
+ : "";
343
+ return [
344
+ `${index}. ${providerTag} ${s.id} — ${s.cpu}${coreStr} | ${s.ram_gb} GB RAM${ecc} | ${disk} | ${s.datacenter}`,
345
+ ` \u20AC${s.price_monthly_eur.toFixed(2)}/mo${hourly} | ${priceInfo}${gpu}`,
346
+ ` ${s.url}`,
347
+ ``,
348
+ ].join("\n");
349
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/build/utils.js ADDED
@@ -0,0 +1,50 @@
1
+ export function getCpuVendor(cpu) {
2
+ const lower = cpu.toLowerCase();
3
+ if (lower.includes("intel") || lower.includes("xeon") || lower.includes("core i"))
4
+ return "intel";
5
+ if (lower.includes("amd") || lower.includes("ryzen") || lower.includes("epyc"))
6
+ return "amd";
7
+ return "unknown";
8
+ }
9
+ export function formatSize(gb) {
10
+ if (gb >= 1000)
11
+ return `${(gb / 1000).toFixed(gb % 1000 === 0 ? 0 : 1)} TB`;
12
+ return `${gb} GB`;
13
+ }
14
+ export function formatDiskSummary(listing) {
15
+ if (listing.disks.length === 0)
16
+ return "No disks";
17
+ const parts = listing.disks.map((d) => {
18
+ const typeLabel = d.type === "nvme" ? "NVMe"
19
+ : d.type === "sata" ? "SATA SSD"
20
+ : d.type === "hdd" ? "HDD"
21
+ : "Disk";
22
+ return `${d.count}x ${formatSize(d.size_gb)} ${typeLabel}`;
23
+ });
24
+ return parts.join(" + ");
25
+ }
26
+ export function getDiskType(listing) {
27
+ const types = new Set(listing.disks.map((d) => d.type).filter((t) => t !== "unknown"));
28
+ if (types.size === 0)
29
+ return "none";
30
+ if (types.size > 1)
31
+ return "mixed";
32
+ return types.values().next().value;
33
+ }
34
+ export function formatBandwidth(mbps) {
35
+ if (mbps >= 1000)
36
+ return `${mbps / 1000} Gbit`;
37
+ return `${mbps} Mbit`;
38
+ }
39
+ export function formatNextReduce(listing) {
40
+ if (listing.fixed_price || listing.next_reduce_timestamp === null)
41
+ return "Fixed price";
42
+ const remaining = listing.next_reduce_timestamp * 1000 - Date.now();
43
+ if (remaining <= 0)
44
+ return "Reduction imminent";
45
+ const hours = Math.floor(remaining / 3600000);
46
+ const mins = Math.floor((remaining % 3600000) / 60000);
47
+ if (hours > 0)
48
+ return `Next reduction in ${hours}h ${mins}m`;
49
+ return `Next reduction in ${mins}m`;
50
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mcp-server-auction",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for European dedicated server auctions and listings",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "mcp-server-auction": "build/index.js"
9
+ },
10
+ "files": [
11
+ "build"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node build/index.js",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "dedicated-server",
22
+ "hetzner",
23
+ "ovh",
24
+ "server-auction"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/raws-labs/mcp-server-auction.git"
29
+ },
30
+ "homepage": "https://github.com/raws-labs/mcp-server-auction",
31
+ "bugs": {
32
+ "url": "https://github.com/raws-labs/mcp-server-auction/issues"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.12.1",
36
+ "zod": "^3.24.4"
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "^5.8.3",
40
+ "@types/node": "^22.15.3"
41
+ },
42
+ "license": "MIT"
43
+ }