pixel-surgeon-mcp 1.0.0 → 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/dist/index.js +119 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,15 +39,16 @@ function loadKeysFromClaudeConfig() {
|
|
|
39
39
|
const configPath = join(homedir(), ".claude.json");
|
|
40
40
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
41
41
|
const env = config?.mcpServers?.["pixel-surgeon"]?.env ?? {};
|
|
42
|
-
return { google: env.GOOGLE_API_KEY ?? "", openai: env.OPENAI_API_KEY ?? "" };
|
|
42
|
+
return { google: env.GOOGLE_API_KEY ?? "", openai: env.OPENAI_API_KEY ?? "", xai: env.XAI_API_KEY ?? "" };
|
|
43
43
|
}
|
|
44
44
|
catch {
|
|
45
|
-
return { google: "", openai: "" };
|
|
45
|
+
return { google: "", openai: "", xai: "" };
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
const _claudeKeys = (!process.env.GOOGLE_API_KEY && !process.env.OPENAI_API_KEY) ? loadKeysFromClaudeConfig() : { google: "", openai: "" };
|
|
48
|
+
const _claudeKeys = (!process.env.GOOGLE_API_KEY && !process.env.OPENAI_API_KEY && !process.env.XAI_API_KEY) ? loadKeysFromClaudeConfig() : { google: "", openai: "", xai: "" };
|
|
49
49
|
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || _claudeKeys.google;
|
|
50
50
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || _claudeKeys.openai;
|
|
51
|
+
const XAI_API_KEY = process.env.XAI_API_KEY || _claudeKeys.xai;
|
|
51
52
|
const MODELS = {
|
|
52
53
|
"gemini-3.1-flash-image": {
|
|
53
54
|
id: "gemini-3.1-flash-image-preview",
|
|
@@ -73,6 +74,12 @@ const MODELS = {
|
|
|
73
74
|
provider: "openai",
|
|
74
75
|
tier: "paid",
|
|
75
76
|
},
|
|
77
|
+
"grok-imagine": {
|
|
78
|
+
id: "grok-imagine-image-quality",
|
|
79
|
+
label: "Grok Imagine (xAI)",
|
|
80
|
+
provider: "xai",
|
|
81
|
+
tier: "paid",
|
|
82
|
+
},
|
|
76
83
|
};
|
|
77
84
|
const MODEL_KEYS = Object.keys(MODELS);
|
|
78
85
|
const GEMINI_DEFAULT = "gemini-3.1-flash-image";
|
|
@@ -99,7 +106,7 @@ function getProvider(modelKey) {
|
|
|
99
106
|
throw new Error(`Unknown model "${key}". Available: ${MODEL_KEYS.join(", ")}`);
|
|
100
107
|
const provider = providers[entry.provider];
|
|
101
108
|
if (!provider) {
|
|
102
|
-
const envHint = entry.provider === "gemini" ? "GOOGLE_API_KEY" : "OPENAI_API_KEY";
|
|
109
|
+
const envHint = entry.provider === "gemini" ? "GOOGLE_API_KEY" : entry.provider === "xai" ? "XAI_API_KEY" : "OPENAI_API_KEY";
|
|
103
110
|
throw new Error(`Provider "${entry.provider}" not available. Set ${envHint} env var.`);
|
|
104
111
|
}
|
|
105
112
|
return { provider, modelId: entry.id, modelKey: key };
|
|
@@ -1433,6 +1440,104 @@ class OpenAIProvider {
|
|
|
1433
1440
|
return { imageBase64, text: "", modelUsed: req.modelId };
|
|
1434
1441
|
}
|
|
1435
1442
|
}
|
|
1443
|
+
const GROK_ASPECT_MAP = {
|
|
1444
|
+
"1:1": "1:1", "16:9": "16:9", "9:16": "9:16",
|
|
1445
|
+
"3:4": "3:4", "4:3": "4:3", "2:3": "2:3", "3:2": "3:2",
|
|
1446
|
+
"4:5": "3:4", "5:4": "4:3",
|
|
1447
|
+
};
|
|
1448
|
+
function grokAspect(aspectRatio) {
|
|
1449
|
+
return GROK_ASPECT_MAP[aspectRatio] ?? "1:1";
|
|
1450
|
+
}
|
|
1451
|
+
class GrokProvider {
|
|
1452
|
+
name = "xai";
|
|
1453
|
+
async generate(req) {
|
|
1454
|
+
const aspect = grokAspect(req.aspectRatio);
|
|
1455
|
+
const t0 = Date.now();
|
|
1456
|
+
log(` Calling xAI ${req.modelId} generate (aspect=${aspect})...`);
|
|
1457
|
+
let res;
|
|
1458
|
+
try {
|
|
1459
|
+
res = await fetch("https://api.x.ai/v1/images/generations", {
|
|
1460
|
+
method: "POST",
|
|
1461
|
+
headers: {
|
|
1462
|
+
"Content-Type": "application/json",
|
|
1463
|
+
"Authorization": `Bearer ${XAI_API_KEY}`,
|
|
1464
|
+
},
|
|
1465
|
+
body: JSON.stringify({
|
|
1466
|
+
model: req.modelId,
|
|
1467
|
+
prompt: req.prompt,
|
|
1468
|
+
n: 1,
|
|
1469
|
+
aspect_ratio: aspect,
|
|
1470
|
+
response_format: "b64_json",
|
|
1471
|
+
}),
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
catch (fetchErr) {
|
|
1475
|
+
throw new Error(`Network error calling xAI API: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
1476
|
+
}
|
|
1477
|
+
const elapsed = Date.now() - t0;
|
|
1478
|
+
log(` xAI responded HTTP ${res.status} in ${(elapsed / 1000).toFixed(1)}s`);
|
|
1479
|
+
const rawBody = await res.text();
|
|
1480
|
+
let data;
|
|
1481
|
+
try {
|
|
1482
|
+
data = JSON.parse(rawBody);
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
throw new Error(`xAI API returned non-JSON (HTTP ${res.status}). Raw body: ${rawBody.slice(0, 2000)}`);
|
|
1486
|
+
}
|
|
1487
|
+
if (!res.ok || data.error) {
|
|
1488
|
+
throw new Error(`xAI API HTTP ${res.status}: ${data.error?.message ?? rawBody.slice(0, 2000)}`);
|
|
1489
|
+
}
|
|
1490
|
+
const imageBase64 = data.data?.[0]?.b64_json;
|
|
1491
|
+
if (!imageBase64) {
|
|
1492
|
+
throw new Error(`xAI returned no image data. Response: ${rawBody.slice(0, 2000)}`);
|
|
1493
|
+
}
|
|
1494
|
+
log(` Got image: ${(imageBase64.length / 1024).toFixed(0)}KB base64 from ${req.modelId}`);
|
|
1495
|
+
return { imageBase64, text: "", modelUsed: req.modelId };
|
|
1496
|
+
}
|
|
1497
|
+
async edit(req) {
|
|
1498
|
+
const t0 = Date.now();
|
|
1499
|
+
log(` Calling xAI ${req.modelId} edit...`);
|
|
1500
|
+
let res;
|
|
1501
|
+
try {
|
|
1502
|
+
res = await fetch("https://api.x.ai/v1/images/edits", {
|
|
1503
|
+
method: "POST",
|
|
1504
|
+
headers: {
|
|
1505
|
+
"Content-Type": "application/json",
|
|
1506
|
+
"Authorization": `Bearer ${XAI_API_KEY}`,
|
|
1507
|
+
},
|
|
1508
|
+
body: JSON.stringify({
|
|
1509
|
+
model: req.modelId,
|
|
1510
|
+
prompt: req.prompt,
|
|
1511
|
+
image: { url: `data:${req.imageMime};base64,${req.imageBase64}`, type: "image_url" },
|
|
1512
|
+
n: 1,
|
|
1513
|
+
response_format: "b64_json",
|
|
1514
|
+
}),
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
catch (fetchErr) {
|
|
1518
|
+
throw new Error(`Network error calling xAI edit API: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
1519
|
+
}
|
|
1520
|
+
const elapsed = Date.now() - t0;
|
|
1521
|
+
log(` xAI edit responded HTTP ${res.status} in ${(elapsed / 1000).toFixed(1)}s`);
|
|
1522
|
+
const rawBody = await res.text();
|
|
1523
|
+
let data;
|
|
1524
|
+
try {
|
|
1525
|
+
data = JSON.parse(rawBody);
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
throw new Error(`xAI edit API returned non-JSON (HTTP ${res.status}). Raw body: ${rawBody.slice(0, 2000)}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (!res.ok || data.error) {
|
|
1531
|
+
throw new Error(`xAI edit API HTTP ${res.status}: ${data.error?.message ?? rawBody.slice(0, 2000)}`);
|
|
1532
|
+
}
|
|
1533
|
+
const imageBase64 = data.data?.[0]?.b64_json;
|
|
1534
|
+
if (!imageBase64) {
|
|
1535
|
+
throw new Error(`xAI edit returned no image data. Response: ${rawBody.slice(0, 2000)}`);
|
|
1536
|
+
}
|
|
1537
|
+
log(` Got image: ${(imageBase64.length / 1024).toFixed(0)}KB base64 from ${req.modelId} edit`);
|
|
1538
|
+
return { imageBase64, text: "", modelUsed: req.modelId };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1436
1541
|
// --- Veo API ---
|
|
1437
1542
|
async function callVeo(prompt, aspectRatio, durationSeconds) {
|
|
1438
1543
|
const t0 = Date.now();
|
|
@@ -1555,7 +1660,8 @@ const EXPLICIT_FREE_NOTICE = `\u2139\uFE0F Generated with ${MODEL_FALLBACK} (Gem
|
|
|
1555
1660
|
`For higher-quality image generation, pass model='${GEMINI_DEFAULT}' — this requires prepaid credits on your Google AI account.`;
|
|
1556
1661
|
const MODEL_PARAM_DESCRIPTION = `Model to use. Available: ${MODEL_KEYS.map(k => `'${k}' (${MODELS[k].label})`).join(", ")}. ` +
|
|
1557
1662
|
`Default: '${getDefaultModelKey()}'. Set DEFAULT_IMAGE_MODEL env var to change the default. ` +
|
|
1558
|
-
`
|
|
1663
|
+
`Provider tradeoffs: grok-imagine is fastest and cheapest; gemini is mid-quality with the best price/performance ratio (free tier available); gpt-image-2 is highest quality but slower and more expensive. ` +
|
|
1664
|
+
`Gemini models fall back to free tier on billing errors. OpenAI requires OPENAI_API_KEY. Grok requires XAI_API_KEY.`;
|
|
1559
1665
|
function noticeFor(modelUsed, explicitModelKey) {
|
|
1560
1666
|
if (explicitModelKey && MODELS[explicitModelKey]?.provider !== "gemini")
|
|
1561
1667
|
return "";
|
|
@@ -2529,17 +2635,21 @@ async function main() {
|
|
|
2529
2635
|
providers["openai"] = new OpenAIProvider();
|
|
2530
2636
|
log("OpenAI provider available");
|
|
2531
2637
|
}
|
|
2638
|
+
if (XAI_API_KEY) {
|
|
2639
|
+
providers["xai"] = new GrokProvider();
|
|
2640
|
+
log("xAI/Grok provider available");
|
|
2641
|
+
}
|
|
2532
2642
|
if (process.argv.includes("--viewer")) {
|
|
2533
|
-
if (!GOOGLE_API_KEY && !OPENAI_API_KEY) {
|
|
2534
|
-
console.log("Note: No API keys set — viewer is read-only (respin disabled). Set GOOGLE_API_KEY or
|
|
2643
|
+
if (!GOOGLE_API_KEY && !OPENAI_API_KEY && !XAI_API_KEY) {
|
|
2644
|
+
console.log("Note: No API keys set — viewer is read-only (respin disabled). Set GOOGLE_API_KEY, OPENAI_API_KEY, or XAI_API_KEY to enable generation.");
|
|
2535
2645
|
}
|
|
2536
2646
|
viewerPort = await startViewer();
|
|
2537
2647
|
console.log(`pixel-surgeon-mcp viewer running at http://localhost:${viewerPort}`);
|
|
2538
2648
|
openExternal(`http://localhost:${viewerPort}`);
|
|
2539
2649
|
return;
|
|
2540
2650
|
}
|
|
2541
|
-
if (!GOOGLE_API_KEY && !OPENAI_API_KEY) {
|
|
2542
|
-
log("WARNING:
|
|
2651
|
+
if (!GOOGLE_API_KEY && !OPENAI_API_KEY && !XAI_API_KEY) {
|
|
2652
|
+
log("WARNING: No API keys set (GOOGLE_API_KEY, OPENAI_API_KEY, XAI_API_KEY). No image providers available.");
|
|
2543
2653
|
}
|
|
2544
2654
|
log(`Default model: ${getDefaultModelKey()}`);
|
|
2545
2655
|
const transport = new StdioServerTransport();
|