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.
Files changed (2) hide show
  1. package/dist/index.js +119 -9
  2. 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
- `Gemini models fall back to free tier on billing errors. OpenAI requires OPENAI_API_KEY.`;
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 OPENAI_API_KEY to enable generation.");
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: Neither GOOGLE_API_KEY nor OPENAI_API_KEY is set. No image providers available.");
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixel-surgeon-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "mcpName": "io.github.j-east/pixel-surgeon",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",