forgemesh-imagegen 2.0.2 → 2.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/README.md +15 -9
- package/index.js +108 -151
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# forgemesh-imagegen
|
|
2
2
|
|
|
3
3
|
[](https://m8ven.ai/mcp/forgemeshlabs-imagegen-mcp-5nc38d)
|
|
4
|
+
[](https://mcpservers.org/servers/forgemeshlabs/imagegen-mcp)
|
|
4
5
|
|
|
5
6
|
**MCP server for AI image generation.** Generate images, remove backgrounds, upscale to 4x HD — all from a single MCP tool call. Payments handled automatically in USDC on Base mainnet via x402. No API key. No subscription. Pay per image.
|
|
6
7
|
|
|
@@ -16,9 +17,11 @@ Part of the [ForgeMesh](https://github.com/forgemeshlabs/forgemesh) ecosystem
|
|
|
16
17
|
| `generate_clean` | Generate + background removal | $0.35 USDC |
|
|
17
18
|
| `generate_hd` | Premium generate + 4x upscale (HD) | $0.50 USDC |
|
|
18
19
|
| `generate_pro` | Top-tier generate + bg removal + 4x upscale | $0.75 USDC |
|
|
20
|
+
| `list_presets` | List the 58 identity-preserving transform presets (person or pet) | $0.005 USDC |
|
|
21
|
+
| `transform_image` | Transform a photo with a preset while keeping the subject's identity (FLUX Kontext Max) | $0.75 USDC |
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
`
|
|
23
|
+
The four `generate_*` tools accept `prompt` (required) and `aspect` (optional: `1:1`, `16:9`, `9:16`, `4:3`).
|
|
24
|
+
`transform_image` takes a public `image_url`, a `preset` id from `list_presets`, an optional `aspect` (`match_input_image` default, or `1:1`, `16:9`, `9:16`, `4:3`, `3:4`) and `wait` (default true: polls the free status URL until the PNG is ready, usually 15-30s). Submit only images you have rights to; no minors; the provider safety filter applies.
|
|
22
25
|
|
|
23
26
|
---
|
|
24
27
|
|
|
@@ -35,8 +38,7 @@ Add to `claude_desktop_config.json`:
|
|
|
35
38
|
"command": "npx",
|
|
36
39
|
"args": ["-y", "forgemesh-imagegen"],
|
|
37
40
|
"env": {
|
|
38
|
-
"WALLET_PRIVATE_KEY": "0x..."
|
|
39
|
-
"PYRIMID_AFFILIATE_ID": "af_your_id"
|
|
41
|
+
"WALLET_PRIVATE_KEY": "0x..."
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -58,8 +60,8 @@ export WALLET_PRIVATE_KEY=0x...
|
|
|
58
60
|
|
|
59
61
|
## Requirements
|
|
60
62
|
|
|
61
|
-
- A Base mainnet wallet private key with USDC
|
|
62
|
-
- $1 USDC ≈ 4 base images, 2 clean, 2 HD, or 1 pro
|
|
63
|
+
- A **dedicated, low-balance** Base mainnet wallet private key with USDC (never your primary wallet: the server signs USDC transfers autonomously)
|
|
64
|
+
- $1 USDC ≈ 4 base images, 2 clean, 2 HD, or 1 pro, or 1 transform
|
|
63
65
|
- No other API keys needed
|
|
64
66
|
|
|
65
67
|
Get USDC on Base: [Coinbase](https://coinbase.com) → Bridge to Base, or buy directly on Base.
|
|
@@ -72,9 +74,6 @@ Get USDC on Base: [Coinbase](https://coinbase.com) → Bridge to Base, or buy di
|
|
|
72
74
|
generate_image(prompt="a red panda in a spacesuit", aspect="1:1")
|
|
73
75
|
→ { image_url: "https://...", prompt: "...", aspect: "1:1", tier: "image" }
|
|
74
76
|
|
|
75
|
-
generate_image(prompt="a red panda in a spacesuit", affiliate_id="af_your_id")
|
|
76
|
-
→ routes through Pyrimid product 3 with no extra cost to the caller
|
|
77
|
-
|
|
78
77
|
generate_clean(prompt="a product photo of a ceramic mug")
|
|
79
78
|
→ { image_url: "https://...", tier: "clean" } // transparent PNG
|
|
80
79
|
|
|
@@ -83,6 +82,12 @@ generate_hd(prompt="a futuristic city at night, cyberpunk style")
|
|
|
83
82
|
|
|
84
83
|
generate_pro(prompt="a logo mark, geometric eagle")
|
|
85
84
|
→ { image_url: "https://...", tier: "pro" } // transparent + HD
|
|
85
|
+
|
|
86
|
+
list_presets()
|
|
87
|
+
→ { count: 58, presets: [{ id: "pet-royal", title: "Pet Royalty", subject: "pet", ... }, ...] }
|
|
88
|
+
|
|
89
|
+
transform_image(image_url="https://example.com/my-dog.jpg", preset="pet-royal")
|
|
90
|
+
→ { job_id: "...", status: "succeeded", preset: "pet-royal", image_url: "https://..." }
|
|
86
91
|
```
|
|
87
92
|
|
|
88
93
|
---
|
|
@@ -94,6 +99,7 @@ Each tool call makes an HTTP request to the ForgeMesh imagegen service gated by
|
|
|
94
99
|
- Network: Base mainnet (eip155:8453)
|
|
95
100
|
- Token: USDC
|
|
96
101
|
- Protocol: [x402](https://x402.org)
|
|
102
|
+
- The server only ever connects to `https://imagegen.coinopai.com`; every request has a 60s timeout and a 1 MB response cap; no third-party payment routers or affiliate hops.
|
|
97
103
|
|
|
98
104
|
---
|
|
99
105
|
|
package/index.js
CHANGED
|
@@ -8,113 +8,87 @@ const { x402Client, x402HTTPClient } = require("@x402/core/client");
|
|
|
8
8
|
const { ExactEvmScheme } = require("@x402/evm/exact/client");
|
|
9
9
|
const { toClientEvmSigner } = require("@x402/evm");
|
|
10
10
|
const { privateKeyToAccount } = require("viem/accounts");
|
|
11
|
-
const { createPublicClient, createWalletClient, http, parseAbi } = require("viem");
|
|
12
|
-
const { base } = require("viem/chains");
|
|
13
11
|
|
|
14
12
|
const BASE_URL = "https://imagegen.coinopai.com";
|
|
15
13
|
const VALID_ASPECTS = ["1:1", "16:9", "9:16", "4:3"];
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
14
|
+
const TRANSFORM_ASPECTS = ["match_input_image", "1:1", "16:9", "9:16", "4:3", "3:4"];
|
|
15
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
16
|
+
const MAX_RESPONSE_BYTES = 1_000_000;
|
|
17
|
+
const MAX_PROMPT_CHARS = 2_000;
|
|
18
|
+
|
|
19
|
+
const GENERATE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
20
|
+
const promptSchema = (subject) => ({
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
prompt: { type: "string", maxLength: MAX_PROMPT_CHARS, description: subject },
|
|
24
|
+
aspect: { type: "string", enum: VALID_ASPECTS, description: "Aspect ratio — 1:1 (default), 16:9, 9:16, 4:3" },
|
|
25
|
+
},
|
|
26
|
+
required: ["prompt"],
|
|
27
|
+
});
|
|
28
28
|
|
|
29
29
|
const TOOLS = [
|
|
30
30
|
{
|
|
31
31
|
name: "generate_image",
|
|
32
|
-
annotations:
|
|
33
|
-
readOnlyHint: false,
|
|
34
|
-
destructiveHint: false,
|
|
35
|
-
idempotentHint: false,
|
|
36
|
-
openWorldHint: true,
|
|
37
|
-
},
|
|
32
|
+
annotations: GENERATE_ANNOTATIONS,
|
|
38
33
|
description: "Generate a standard AI image from a text prompt. Returns a PNG image URL. Costs $0.25 USDC on Base mainnet — paid automatically.",
|
|
39
|
-
inputSchema:
|
|
40
|
-
type: "object",
|
|
41
|
-
properties: {
|
|
42
|
-
prompt: { type: "string", description: "Text description of the image to generate" },
|
|
43
|
-
aspect: { type: "string", enum: VALID_ASPECTS, description: "Aspect ratio — 1:1 (default), 16:9, 9:16, 4:3" },
|
|
44
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID. Affiliate earns a commission from within the listed price — no extra cost to you." },
|
|
45
|
-
},
|
|
46
|
-
required: ["prompt"],
|
|
47
|
-
},
|
|
34
|
+
inputSchema: promptSchema("Text description of the image to generate"),
|
|
48
35
|
},
|
|
49
36
|
{
|
|
50
37
|
name: "generate_clean",
|
|
51
|
-
annotations:
|
|
52
|
-
readOnlyHint: false,
|
|
53
|
-
destructiveHint: false,
|
|
54
|
-
idempotentHint: false,
|
|
55
|
-
openWorldHint: true,
|
|
56
|
-
},
|
|
38
|
+
annotations: GENERATE_ANNOTATIONS,
|
|
57
39
|
description: "Generate an AI image with background removed. Returns a transparent PNG URL. Costs $0.35 USDC on Base mainnet.",
|
|
58
|
-
inputSchema:
|
|
59
|
-
type: "object",
|
|
60
|
-
properties: {
|
|
61
|
-
prompt: { type: "string", description: "Text description of the subject (background will be removed)" },
|
|
62
|
-
aspect: { type: "string", enum: VALID_ASPECTS, description: "Aspect ratio — 1:1 (default), 16:9, 9:16, 4:3" },
|
|
63
|
-
},
|
|
64
|
-
required: ["prompt"],
|
|
65
|
-
},
|
|
40
|
+
inputSchema: promptSchema("Text description of the subject (background will be removed)"),
|
|
66
41
|
},
|
|
67
42
|
{
|
|
68
43
|
name: "generate_hd",
|
|
69
|
-
annotations:
|
|
70
|
-
readOnlyHint: false,
|
|
71
|
-
destructiveHint: false,
|
|
72
|
-
idempotentHint: false,
|
|
73
|
-
openWorldHint: true,
|
|
74
|
-
},
|
|
44
|
+
annotations: GENERATE_ANNOTATIONS,
|
|
75
45
|
description: "Generate a premium AI image upscaled 4x HD. Returns a high-resolution image URL. Costs $0.50 USDC on Base mainnet.",
|
|
76
|
-
inputSchema:
|
|
77
|
-
type: "object",
|
|
78
|
-
properties: {
|
|
79
|
-
prompt: { type: "string", description: "Text description of the image to generate" },
|
|
80
|
-
aspect: { type: "string", enum: VALID_ASPECTS, description: "Aspect ratio — 1:1 (default), 16:9, 9:16, 4:3" },
|
|
81
|
-
},
|
|
82
|
-
required: ["prompt"],
|
|
83
|
-
},
|
|
46
|
+
inputSchema: promptSchema("Text description of the image to generate"),
|
|
84
47
|
},
|
|
85
48
|
{
|
|
86
49
|
name: "generate_pro",
|
|
87
|
-
annotations:
|
|
88
|
-
readOnlyHint: false,
|
|
89
|
-
destructiveHint: false,
|
|
90
|
-
idempotentHint: false,
|
|
91
|
-
openWorldHint: true,
|
|
92
|
-
},
|
|
50
|
+
annotations: GENERATE_ANNOTATIONS,
|
|
93
51
|
description: "Generate a top-tier AI image with background removal and 4x HD upscale. Costs $0.75 USDC on Base mainnet.",
|
|
52
|
+
inputSchema: promptSchema("Text description of the subject (background removed, then upscaled)"),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "list_presets",
|
|
56
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
57
|
+
description: "List the 58 identity-preserving transform presets for transform_image (action figure, pet royalty, CEO portrait, cartoon villain, fantasy poster, pet astronaut, ...). Returns id, title, tagline and whether each expects a person or a pet photo. Costs $0.005 USDC on Base mainnet.",
|
|
58
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "transform_image",
|
|
62
|
+
annotations: GENERATE_ANNOTATIONS,
|
|
63
|
+
description: "Transform a photo with a named preset while keeping the subject's identity (FLUX Kontext Max image-to-image). Pass a public https image URL and a preset id from list_presets. Returns a job with a free status_url to poll until the PNG is ready (usually 15-30s). Costs $0.75 USDC on Base mainnet. Submit only images you have rights to; no minors; provider safety filter applies.",
|
|
94
64
|
inputSchema: {
|
|
95
65
|
type: "object",
|
|
96
66
|
properties: {
|
|
97
|
-
|
|
98
|
-
|
|
67
|
+
image_url: { type: "string", format: "uri", maxLength: 2048, description: "Public https URL of the source photo (JPEG, PNG or WebP)" },
|
|
68
|
+
preset: { type: "string", pattern: "^[a-z0-9-]{1,64}$", description: "Preset id from list_presets, e.g. action-figure, pet-royal, ceo-portrait" },
|
|
69
|
+
aspect: { type: "string", enum: TRANSFORM_ASPECTS, description: "Output aspect (default match_input_image)" },
|
|
70
|
+
wait: { type: "boolean", description: "Poll the job until it finishes and return the final image_url (default true, up to ~90s)" },
|
|
99
71
|
},
|
|
100
|
-
required: ["
|
|
72
|
+
required: ["image_url", "preset"],
|
|
101
73
|
},
|
|
102
74
|
},
|
|
103
75
|
];
|
|
104
76
|
|
|
105
77
|
const TOOL_ENDPOINTS = {
|
|
106
|
-
generate_image:
|
|
107
|
-
generate_clean:
|
|
108
|
-
generate_hd:
|
|
109
|
-
generate_pro:
|
|
78
|
+
generate_image: { path: "/generate", price: "$0.25" },
|
|
79
|
+
generate_clean: { path: "/generate/clean", price: "$0.35" },
|
|
80
|
+
generate_hd: { path: "/generate/hd", price: "$0.50" },
|
|
81
|
+
generate_pro: { path: "/generate/pro", price: "$0.75" },
|
|
82
|
+
list_presets: { path: "/presets", price: "$0.005" },
|
|
83
|
+
transform_image: { path: "/transform", price: "$0.75" },
|
|
110
84
|
};
|
|
111
85
|
|
|
112
86
|
function buildHttpClient() {
|
|
113
87
|
const key = process.env.WALLET_PRIVATE_KEY;
|
|
114
88
|
if (!key) {
|
|
115
89
|
throw new Error(
|
|
116
|
-
"WALLET_PRIVATE_KEY required — set a Base wallet private key funded with USDC.\n" +
|
|
117
|
-
" generate_image = $0.25 | generate_clean = $0.35 | generate_hd = $0.50 | generate_pro = $0.75"
|
|
90
|
+
"WALLET_PRIVATE_KEY required — set a dedicated, low-balance Base wallet private key funded with USDC.\n" +
|
|
91
|
+
" generate_image = $0.25 | generate_clean = $0.35 | generate_hd = $0.50 | generate_pro = $0.75 | transform_image = $0.75"
|
|
118
92
|
);
|
|
119
93
|
}
|
|
120
94
|
const pk = key.startsWith("0x") ? key : "0x" + key;
|
|
@@ -124,84 +98,55 @@ function buildHttpClient() {
|
|
|
124
98
|
return { httpClient: new x402HTTPClient(coreClient), account };
|
|
125
99
|
}
|
|
126
100
|
|
|
127
|
-
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
128
|
-
|
|
129
|
-
async function callPyrimid(account, url, product, affiliateId) {
|
|
130
|
-
const transport = http();
|
|
131
|
-
const publicClient = createPublicClient({ chain: base, transport });
|
|
132
|
-
const walletClient = createWalletClient({ account, chain: base, transport });
|
|
101
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
133
102
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await publicClient.waitForTransactionReceipt({ hash: routeHash });
|
|
150
|
-
await sleep(3000);
|
|
151
|
-
|
|
152
|
-
const paidRes = await fetch(url.toString(), {
|
|
153
|
-
headers: { "X-Affiliate-ID": affiliateId, "X-Payment": routeHash },
|
|
154
|
-
});
|
|
155
|
-
if (!paidRes.ok) {
|
|
156
|
-
const errBody = await paidRes.text().catch(() => paidRes.statusText);
|
|
157
|
-
throw new Error(`Pyrimid payment failed — HTTP ${paidRes.status}: ${errBody.slice(0, 200)}`);
|
|
103
|
+
// Every outbound request: bounded time, bounded body, same-origin only.
|
|
104
|
+
async function fetchBounded(url, init = {}) {
|
|
105
|
+
const target = new URL(String(url));
|
|
106
|
+
if (target.origin !== BASE_URL) throw new Error(`Refusing to call ${target.origin}; this server only talks to ${BASE_URL}`);
|
|
107
|
+
const ctrl = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(target.toString(), { ...init, signal: ctrl.signal, redirect: "error" });
|
|
111
|
+
const declared = Number(res.headers.get("content-length") || 0);
|
|
112
|
+
if (declared > MAX_RESPONSE_BYTES) throw new Error(`Response too large (${declared} bytes)`);
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
if (text.length > MAX_RESPONSE_BYTES) throw new Error("Response too large");
|
|
115
|
+
return { status: res.status, ok: res.ok, headers: res.headers, text };
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
158
118
|
}
|
|
159
|
-
return paidRes.json();
|
|
160
119
|
}
|
|
161
120
|
|
|
162
|
-
|
|
163
|
-
|
|
121
|
+
function parseJson(text, status) {
|
|
122
|
+
try { return JSON.parse(text); } catch { throw new Error(`HTTP ${status}: non-JSON response`); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function callPaid(ctx, path, params) {
|
|
126
|
+
const { httpClient } = ctx;
|
|
164
127
|
const url = new URL(BASE_URL + path);
|
|
165
128
|
if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
166
129
|
|
|
167
|
-
const
|
|
168
|
-
if (!product) affiliateId = null;
|
|
169
|
-
const extraHeaders = affiliateId ? { "X-Affiliate-ID": affiliateId } : {};
|
|
170
|
-
|
|
171
|
-
if (affiliateId && product) {
|
|
172
|
-
try {
|
|
173
|
-
return await callPyrimid(account, url, product, affiliateId);
|
|
174
|
-
} catch (_) {
|
|
175
|
-
affiliateId = null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const directHeaders = affiliateId ? extraHeaders : {};
|
|
180
|
-
const res = await fetch(url.toString(), { headers: directHeaders });
|
|
181
|
-
|
|
130
|
+
const res = await fetchBounded(url);
|
|
182
131
|
if (res.status === 402) {
|
|
183
132
|
let body;
|
|
184
|
-
try { body =
|
|
185
|
-
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
186
|
-
(name) => res.headers.get(name),
|
|
187
|
-
body
|
|
188
|
-
);
|
|
133
|
+
try { body = JSON.parse(res.text); } catch { body = undefined; }
|
|
134
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse((name) => res.headers.get(name), body);
|
|
189
135
|
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!paidRes.ok) {
|
|
194
|
-
const errBody = await paidRes.text().catch(() => paidRes.statusText);
|
|
195
|
-
throw new Error(`Payment failed — HTTP ${paidRes.status}: ${errBody.slice(0, 200)}`);
|
|
196
|
-
}
|
|
197
|
-
return paidRes.json();
|
|
136
|
+
const paid = await fetchBounded(url, { headers: httpClient.encodePaymentSignatureHeader(paymentPayload) });
|
|
137
|
+
if (!paid.ok) throw new Error(`Payment failed — HTTP ${paid.status}: ${paid.text.slice(0, 200)}`);
|
|
138
|
+
return parseJson(paid.text, paid.status);
|
|
198
139
|
}
|
|
140
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.text.slice(0, 200)}`);
|
|
141
|
+
return parseJson(res.text, res.status);
|
|
142
|
+
}
|
|
199
143
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
144
|
+
function requirePrompt(args) {
|
|
145
|
+
if (!args || typeof args.prompt !== "string" || !args.prompt.trim()) throw new Error("prompt is required and must be a non-empty string");
|
|
146
|
+
if (args.prompt.length > MAX_PROMPT_CHARS) throw new Error(`prompt must be at most ${MAX_PROMPT_CHARS} characters`);
|
|
147
|
+
const aspect = args.aspect || "1:1";
|
|
148
|
+
if (!VALID_ASPECTS.includes(aspect)) throw new Error(`Invalid aspect ratio '${aspect}'. Valid: ${VALID_ASPECTS.join(", ")}`);
|
|
149
|
+
return { prompt: args.prompt.trim(), aspect };
|
|
205
150
|
}
|
|
206
151
|
|
|
207
152
|
async function main() {
|
|
@@ -214,33 +159,45 @@ async function main() {
|
|
|
214
159
|
}
|
|
215
160
|
|
|
216
161
|
const server = new Server(
|
|
217
|
-
{ name: "forgemesh-imagegen", version: "
|
|
162
|
+
{ name: "forgemesh-imagegen", version: require("./package.json").version },
|
|
218
163
|
{ capabilities: { tools: {} } }
|
|
219
164
|
);
|
|
220
165
|
|
|
221
166
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
222
167
|
|
|
223
168
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
224
|
-
const { name, arguments: args } = req.params;
|
|
169
|
+
const { name, arguments: args = {} } = req.params;
|
|
225
170
|
try {
|
|
226
171
|
const endpoint = TOOL_ENDPOINTS[name];
|
|
227
172
|
if (!endpoint) throw new Error("Unknown tool: " + name);
|
|
228
173
|
|
|
229
|
-
if (
|
|
230
|
-
|
|
174
|
+
if (name === "list_presets") {
|
|
175
|
+
const data = await callPaid(ctx, endpoint.path, null);
|
|
176
|
+
return { content: [{ type: "text", text: JSON.stringify({ count: data.count, presets: data.presets, usage: data.usage, terms: data.terms }, null, 2) }] };
|
|
231
177
|
}
|
|
232
178
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
179
|
+
if (name === "transform_image") {
|
|
180
|
+
if (typeof args.image_url !== "string" || !/^https:\/\/[^\s]{1,2040}$/.test(args.image_url)) throw new Error("image_url must be a public https URL");
|
|
181
|
+
if (typeof args.preset !== "string" || !/^[a-z0-9-]{1,64}$/.test(args.preset)) throw new Error("preset must be a preset id from list_presets");
|
|
182
|
+
const taspect = args.aspect || "match_input_image";
|
|
183
|
+
if (!TRANSFORM_ASPECTS.includes(taspect)) throw new Error(`Invalid aspect '${taspect}'. Valid: ${TRANSFORM_ASPECTS.join(", ")}`);
|
|
184
|
+
const job = await callPaid(ctx, endpoint.path, { image_url: args.image_url, preset: args.preset, aspect: taspect });
|
|
185
|
+
let result = job;
|
|
186
|
+
if (args.wait !== false && typeof job.status_url === "string") {
|
|
187
|
+
for (let i = 0; i < 30 && !["succeeded", "failed"].includes(result.status); i++) {
|
|
188
|
+
await sleep(3000);
|
|
189
|
+
const poll = await fetchBounded(job.status_url);
|
|
190
|
+
if (poll.ok) result = { ...job, ...parseJson(poll.text, poll.status) };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify({ job_id: result.job_id, status: result.status, preset: job.preset, image_url: result.image_url || null, error: result.error || null, status_url: job.status_url }, null, 2) }],
|
|
195
|
+
isError: result.status === "failed",
|
|
196
|
+
};
|
|
236
197
|
}
|
|
237
198
|
|
|
238
|
-
const
|
|
239
|
-
const data = await callPaid(ctx, endpoint.path, {
|
|
240
|
-
prompt: args.prompt.trim(),
|
|
241
|
-
aspect,
|
|
242
|
-
}, affiliateId);
|
|
243
|
-
|
|
199
|
+
const { prompt, aspect } = requirePrompt(args);
|
|
200
|
+
const data = await callPaid(ctx, endpoint.path, { prompt, aspect });
|
|
244
201
|
return {
|
|
245
202
|
content: [{
|
|
246
203
|
type: "text",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgemesh-imagegen",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"mcpName": "io.github.forgemeshlabs/imagegen-mcp",
|
|
5
|
-
"description": "MCP server for AI image generation
|
|
5
|
+
"description": "MCP server for AI image generation — generate, clean (bg removal), HD (4x upscale), Pro. Pay per image in USDC via x402 on Base. No API key needed.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"forgemesh-imagegen": "index.js"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"url": "https://github.com/forgemeshlabs/imagegen-mcp/issues"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
52
52
|
"@x402/core": "^2.11.0",
|
|
53
53
|
"@x402/evm": "^2.11.0",
|
|
54
54
|
"viem": "^2.0.0"
|