image-enhancer-mcp 0.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 +63 -0
- package/dist/index.js +728 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# image-enhancer-mcp
|
|
2
|
+
|
|
3
|
+
stdio [MCP](https://modelcontextprotocol.io/) server that lets a merchant's
|
|
4
|
+
Claude (Desktop or Code) drive **Image Enhancer** — generate Shopify-ready
|
|
5
|
+
apparel photography (ghost mannequin, AI model, detail, lifestyle, flat lay,
|
|
6
|
+
premium, video, banner), iterate results, and give feedback — with strict
|
|
7
|
+
product-identity preservation.
|
|
8
|
+
|
|
9
|
+
It bridges stdio JSON-RPC to the Image Enhancer app's HTTP `/mcp` endpoint using
|
|
10
|
+
the merchant's linking token.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Get your linking token from **Image Enhancer → Settings → Connect with Claude**
|
|
15
|
+
("Rotate token"). Then:
|
|
16
|
+
|
|
17
|
+
**Claude Desktop** — add to your MCP config:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"image-enhancer": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "image-enhancer-mcp"],
|
|
25
|
+
"env": {
|
|
26
|
+
"IE_API_URL": "https://your-app.up.railway.app",
|
|
27
|
+
"IE_MCP_TOKEN": "ie_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Claude Code:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add image-enhancer \
|
|
38
|
+
--env IE_API_URL=https://your-app.up.railway.app \
|
|
39
|
+
--env IE_MCP_TOKEN=ie_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
|
|
40
|
+
-- npx -y image-enhancer-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Environment
|
|
44
|
+
|
|
45
|
+
| Var | Required | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `IE_API_URL` | yes | Base URL of your Image Enhancer app. |
|
|
48
|
+
| `IE_MCP_TOKEN` | yes | Your `ie_<32hex>` linking token. |
|
|
49
|
+
|
|
50
|
+
## Publishing (maintainers)
|
|
51
|
+
|
|
52
|
+
This package is build-ready (`tsup` → `dist/index.js`, executable, deps kept
|
|
53
|
+
external). The actual publish is a deliberate, manual step:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm --filter image-enhancer-mcp build # prepack runs this automatically
|
|
57
|
+
npm pack --dry-run # verify the tarball (dist + package.json)
|
|
58
|
+
npm publish # unscoped public package — needs npm auth only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Published unscoped as `image-enhancer-mcp` (no npm org required). If you ever
|
|
62
|
+
> rename it, update the `npx` command in the Settings → "Connect with Claude"
|
|
63
|
+
> panel (`apps/app/app/routes/app.settings.tsx`).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
var TOKEN_RE = /^ie_[0-9a-f]{32}$/;
|
|
8
|
+
function resolveConfig(env = process.env) {
|
|
9
|
+
const rawUrl = (env.IE_API_URL ?? "").trim();
|
|
10
|
+
const token = (env.IE_MCP_TOKEN ?? "").trim();
|
|
11
|
+
if (!rawUrl) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"IE_API_URL is not set. Add it to your MCP server config \u2014 it is the base URL of your Image Enhancer app (e.g. https://your-app.up.railway.app)."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
let apiUrl;
|
|
17
|
+
try {
|
|
18
|
+
const u = new URL(rawUrl);
|
|
19
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
20
|
+
throw new Error("protocol");
|
|
21
|
+
}
|
|
22
|
+
apiUrl = `${u.origin}${u.pathname.replace(/\/+$/, "").replace(/\/mcp$/, "")}`;
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`IE_API_URL is not a valid http(s) URL: "${rawUrl}". Use the base URL of your Image Enhancer app, e.g. https://your-app.up.railway.app`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!token) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'IE_MCP_TOKEN is not set. Copy your linking token (starts with "ie_") from Image Enhancer \u2192 Settings \u2192 Connect with Claude.'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!TOKEN_RE.test(token)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'IE_MCP_TOKEN is malformed. It must look like "ie_" followed by 32 hex characters. Re-copy it from Image Enhancer \u2192 Settings \u2192 Connect with Claude (use "Rotate token" if it was revoked).'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return { apiUrl, token };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/http-client.ts
|
|
42
|
+
var HttpBridgeClient = class {
|
|
43
|
+
endpoint;
|
|
44
|
+
token;
|
|
45
|
+
nextId = 1;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.endpoint = `${config.apiUrl}/mcp`;
|
|
48
|
+
this.token = config.token;
|
|
49
|
+
}
|
|
50
|
+
async callTool(name, args) {
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(this.endpoint, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "application/json",
|
|
57
|
+
authorization: `Bearer ${this.token}`
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
jsonrpc: "2.0",
|
|
61
|
+
id: this.nextId++,
|
|
62
|
+
method: "tools/call",
|
|
63
|
+
params: { name, arguments: args }
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Could not reach Image Enhancer at ${this.endpoint}. Check IE_API_URL and that the app is running. (${msg})`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (res.status === 401 || res.status === 403) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Image Enhancer rejected the linking token (401/403). Re-copy IE_MCP_TOKEN from Settings \u2192 Connect with Claude, or rotate it if it was revoked."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Image Enhancer returned HTTP ${res.status} ${res.statusText} for tool "${name}".`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
let envelope;
|
|
83
|
+
try {
|
|
84
|
+
envelope = await res.json();
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(`Image Enhancer returned a non-JSON response for tool "${name}".`);
|
|
87
|
+
}
|
|
88
|
+
if (envelope.error) {
|
|
89
|
+
if (envelope.error.code === -32001) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Authentication failed: ${envelope.error.message}. Re-copy IE_MCP_TOKEN from Settings \u2192 Connect with Claude.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(envelope.error.message || `Image Enhancer error on tool "${name}".`);
|
|
95
|
+
}
|
|
96
|
+
return parseToolResult(envelope.result, name);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function parseToolResult(result, name) {
|
|
100
|
+
const text = result?.content?.find((c) => c.type === "text")?.text;
|
|
101
|
+
if (text === void 0) {
|
|
102
|
+
throw new Error(`Image Enhancer returned no content for tool "${name}".`);
|
|
103
|
+
}
|
|
104
|
+
if (result?.isError) {
|
|
105
|
+
throw new Error(text);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(text);
|
|
109
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
110
|
+
return { value: parsed };
|
|
111
|
+
} catch {
|
|
112
|
+
return { message: text };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/server.ts
|
|
117
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
118
|
+
|
|
119
|
+
// src/tools.ts
|
|
120
|
+
import { z } from "zod";
|
|
121
|
+
function toNumericProductId(raw) {
|
|
122
|
+
const v = raw.trim();
|
|
123
|
+
const gid = v.match(/^gid:\/\/shopify\/Product\/(\d+)$/);
|
|
124
|
+
if (gid?.[1]) return gid[1];
|
|
125
|
+
const url = v.match(/\/products\/(\d{6,})(?:[/?#]|$)/);
|
|
126
|
+
if (url?.[1]) return url[1];
|
|
127
|
+
return v;
|
|
128
|
+
}
|
|
129
|
+
var productIdField = z.string().min(1).describe(
|
|
130
|
+
'Shopify product id. Numeric is preferred (e.g. "15002236518511"); a full gid://shopify/Product/<id> or admin /products/<id> URL is also accepted.'
|
|
131
|
+
);
|
|
132
|
+
var additionalImageUrlsField = z.array(z.string().url()).max(8).optional().describe(
|
|
133
|
+
"Optional already-hosted https image URLs (detail close-ups / reference views) passed as extra context to the generator. Attaching raw image bytes from the chat is handled by the agent in a later phase; for now pass URLs."
|
|
134
|
+
);
|
|
135
|
+
var READ = { readOnlyHint: true, openWorldHint: true };
|
|
136
|
+
var WRITE = {
|
|
137
|
+
readOnlyHint: false,
|
|
138
|
+
destructiveHint: false,
|
|
139
|
+
idempotentHint: false,
|
|
140
|
+
openWorldHint: true
|
|
141
|
+
};
|
|
142
|
+
var STATE_CHANGE = {
|
|
143
|
+
readOnlyHint: false,
|
|
144
|
+
destructiveHint: false,
|
|
145
|
+
idempotentHint: true,
|
|
146
|
+
openWorldHint: true
|
|
147
|
+
};
|
|
148
|
+
var jobAcceptedShape = {
|
|
149
|
+
ok: z.boolean().optional(),
|
|
150
|
+
image_job_id: z.string().optional(),
|
|
151
|
+
job_id: z.string().optional(),
|
|
152
|
+
product_id: z.string().optional(),
|
|
153
|
+
derivation: z.string().optional(),
|
|
154
|
+
mode: z.string().optional(),
|
|
155
|
+
also_generate_back: z.boolean().optional(),
|
|
156
|
+
parent_image_job_id: z.string().optional(),
|
|
157
|
+
next: z.string().optional()
|
|
158
|
+
};
|
|
159
|
+
var jobStatusShape = {
|
|
160
|
+
image_job_id: z.string().optional(),
|
|
161
|
+
status: z.string().optional(),
|
|
162
|
+
current_phase: z.string().nullable().optional(),
|
|
163
|
+
version_count: z.number().optional(),
|
|
164
|
+
latest_version_id: z.string().nullable().optional(),
|
|
165
|
+
latest_version_status: z.string().nullable().optional(),
|
|
166
|
+
blocking_validator_failures: z.number().optional()
|
|
167
|
+
};
|
|
168
|
+
var productListShape = {
|
|
169
|
+
count: z.number().optional(),
|
|
170
|
+
products: z.array(z.record(z.unknown())).optional()
|
|
171
|
+
};
|
|
172
|
+
var productStatusShape = {
|
|
173
|
+
state: z.string().optional(),
|
|
174
|
+
product_id: z.string().optional(),
|
|
175
|
+
gm_image_job_id: z.string().nullable().optional(),
|
|
176
|
+
ghost_mannequin: z.record(z.unknown()).optional(),
|
|
177
|
+
ai_model: z.record(z.unknown()).optional(),
|
|
178
|
+
detail: z.record(z.unknown()).optional(),
|
|
179
|
+
lifestyle: z.record(z.unknown()).optional(),
|
|
180
|
+
flat_lay: z.record(z.unknown()).optional()
|
|
181
|
+
};
|
|
182
|
+
var pendingListShape = {
|
|
183
|
+
count: z.number().optional(),
|
|
184
|
+
pending: z.array(z.record(z.unknown())).optional()
|
|
185
|
+
};
|
|
186
|
+
var approveShape = {
|
|
187
|
+
ok: z.boolean().optional(),
|
|
188
|
+
version_id: z.string().optional(),
|
|
189
|
+
status: z.string().optional()
|
|
190
|
+
};
|
|
191
|
+
var productDiscoveryShape = {
|
|
192
|
+
count: z.number().optional(),
|
|
193
|
+
metric: z.string().optional(),
|
|
194
|
+
window_days: z.number().optional(),
|
|
195
|
+
products: z.array(z.record(z.unknown())).optional()
|
|
196
|
+
};
|
|
197
|
+
function buildTools() {
|
|
198
|
+
return [
|
|
199
|
+
// 0a. Best-sellers — the first step of the merchant flow: pick a test set.
|
|
200
|
+
{
|
|
201
|
+
name: "enhancer_suggest_products",
|
|
202
|
+
title: "Suggest best-selling products",
|
|
203
|
+
description: "Suggest the merchant's top products by revenue over a recent window, with each product's current Image Enhancer derivation status. Use this to propose a test set to start with.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
limit: z.number().int().min(1).max(50).optional().describe("How many products to suggest (default 10)."),
|
|
206
|
+
window_days: z.number().int().min(1).max(365).optional().describe("Look-back window in days for the revenue ranking (default 90).")
|
|
207
|
+
},
|
|
208
|
+
outputSchema: productDiscoveryShape,
|
|
209
|
+
annotations: { title: "Suggest best-selling products", ...READ },
|
|
210
|
+
resolve: (input) => ({
|
|
211
|
+
mcpName: "suggest_products",
|
|
212
|
+
args: { limit: input.limit, window_days: input.window_days }
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
// 0b. Search — swap helper.
|
|
216
|
+
{
|
|
217
|
+
name: "enhancer_search_products",
|
|
218
|
+
title: "Search products",
|
|
219
|
+
description: "Free-text search the merchant's catalog (by title / handle / vendor / etc.) to find products to add or swap into a test set.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
query: z.string().min(1).describe("Search text (Shopify product search syntax supported)."),
|
|
222
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
|
|
223
|
+
},
|
|
224
|
+
outputSchema: productDiscoveryShape,
|
|
225
|
+
annotations: { title: "Search products", ...READ },
|
|
226
|
+
resolve: (input) => ({
|
|
227
|
+
mcpName: "search_products",
|
|
228
|
+
args: { query: input.query, limit: input.limit }
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
// 0c. By SKU — swap helper.
|
|
232
|
+
{
|
|
233
|
+
name: "enhancer_list_by_sku",
|
|
234
|
+
title: "List products by SKU",
|
|
235
|
+
description: "Look up products by one or more exact SKUs. Use to assemble or swap a test set by SKU.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
skus: z.array(z.string().min(1)).min(1).max(50).describe("One or more exact SKUs to look up (max 50).")
|
|
238
|
+
},
|
|
239
|
+
outputSchema: productDiscoveryShape,
|
|
240
|
+
annotations: { title: "List products by SKU", ...READ },
|
|
241
|
+
resolve: (input) => ({
|
|
242
|
+
mcpName: "list_by_sku",
|
|
243
|
+
args: { skus: input.skus }
|
|
244
|
+
})
|
|
245
|
+
},
|
|
246
|
+
// 0d. Pre-flight analysis (harness): quality prediction + LOGO resolution
|
|
247
|
+
// before generating. Read-only.
|
|
248
|
+
{
|
|
249
|
+
name: "enhancer_analyze",
|
|
250
|
+
title: "Analyze a product before generating",
|
|
251
|
+
description: `Pre-flight check on a product before you generate: predicts whether the main image will enhance well (with suggestions) and resolves a brand-logo reference (the product's own detail close-up, a "LOGO" product, or none with a suggestion). Run this first to preserve logos and catch bad source photos early.`,
|
|
252
|
+
inputSchema: {
|
|
253
|
+
product_id: productIdField
|
|
254
|
+
},
|
|
255
|
+
outputSchema: {
|
|
256
|
+
product_id: z.string().optional(),
|
|
257
|
+
title: z.string().optional(),
|
|
258
|
+
pre_analysis: z.record(z.unknown()).optional(),
|
|
259
|
+
logo: z.record(z.unknown()).optional(),
|
|
260
|
+
notes: z.array(z.string()).optional()
|
|
261
|
+
},
|
|
262
|
+
annotations: { title: "Analyze a product before generating", ...READ },
|
|
263
|
+
resolve: (input) => ({
|
|
264
|
+
mcpName: "analyze_product",
|
|
265
|
+
args: { product_id: toNumericProductId(input.product_id) }
|
|
266
|
+
})
|
|
267
|
+
},
|
|
268
|
+
// 1. Root Ghost Mannequin — the entry point of the whole flow. Takes a
|
|
269
|
+
// product's main image (fetched server-side via the merchant's OAuth
|
|
270
|
+
// token) and renders the garment on an invisible body.
|
|
271
|
+
{
|
|
272
|
+
name: "enhancer_ghost_mannequin",
|
|
273
|
+
title: "Generate Ghost Mannequin",
|
|
274
|
+
description: "Start a Ghost Mannequin from a Shopify product. Fetches the product's main image via the merchant's connection and renders the garment as if worn by an invisible body. Returns image_job_id \u2014 poll enhancer_job_status until it completes. This is the root image; AI-model / detail / lifestyle / flat-lay / premium derivations all build on the approved Ghost Mannequin.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
product_id: productIdField,
|
|
277
|
+
also_generate_back: z.boolean().optional().describe("Also auto-generate the back view if a back photo is detected (default true)."),
|
|
278
|
+
front_image_url: z.string().url().optional().describe(
|
|
279
|
+
"Override the front source. Must be one of the product's own Shopify image URLs."
|
|
280
|
+
),
|
|
281
|
+
back_image_url: z.string().url().optional().describe(
|
|
282
|
+
"Explicit back-view source. Must be one of the product's own Shopify image URLs."
|
|
283
|
+
),
|
|
284
|
+
additional_image_urls: additionalImageUrlsField,
|
|
285
|
+
style_reference_url: z.string().url().optional().describe(
|
|
286
|
+
'Optional "make it look like this" reference image (desired lighting / mood / composition). The garment identity still comes from the product.'
|
|
287
|
+
),
|
|
288
|
+
logo_reference_url: z.string().url().optional().describe(
|
|
289
|
+
'Optional brand-logo reference image (e.g. from enhancer_analyze) so the logo is preserved. May come from another product (a "LOGO" product).'
|
|
290
|
+
)
|
|
291
|
+
},
|
|
292
|
+
outputSchema: jobAcceptedShape,
|
|
293
|
+
annotations: { title: "Generate Ghost Mannequin", ...WRITE },
|
|
294
|
+
resolve: (input) => ({
|
|
295
|
+
mcpName: "generate_ghost_mannequin",
|
|
296
|
+
args: {
|
|
297
|
+
product_id: toNumericProductId(input.product_id),
|
|
298
|
+
also_generate_back: input.also_generate_back,
|
|
299
|
+
front_image_url: input.front_image_url,
|
|
300
|
+
back_image_url: input.back_image_url,
|
|
301
|
+
additional_image_urls: input.additional_image_urls,
|
|
302
|
+
style_reference_url: input.style_reference_url,
|
|
303
|
+
logo_reference_url: input.logo_reference_url
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
},
|
|
307
|
+
// 2. AI Model
|
|
308
|
+
{
|
|
309
|
+
name: "enhancer_ai_model",
|
|
310
|
+
title: "Generate AI Model shot",
|
|
311
|
+
description: "Render the product worn by a curated virtual model in a chosen pose. Requires an approved Ghost Mannequin for the product. model_id and pose_id are preset ids from the studio. Returns image_job_id.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
product_id: productIdField,
|
|
314
|
+
model_id: z.string().min(1).describe("Curated model profile id."),
|
|
315
|
+
pose_id: z.string().min(1).describe("Pose preset id.")
|
|
316
|
+
},
|
|
317
|
+
outputSchema: jobAcceptedShape,
|
|
318
|
+
annotations: { title: "Generate AI Model shot", ...WRITE },
|
|
319
|
+
resolve: (input) => ({
|
|
320
|
+
mcpName: "generate_derivation",
|
|
321
|
+
args: {
|
|
322
|
+
product_id: toNumericProductId(input.product_id),
|
|
323
|
+
derivation: "ai_model",
|
|
324
|
+
model_id: input.model_id,
|
|
325
|
+
pose_id: input.pose_id
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
},
|
|
329
|
+
// 3. Detail
|
|
330
|
+
{
|
|
331
|
+
name: "enhancer_detail",
|
|
332
|
+
title: "Generate Detail close-up",
|
|
333
|
+
description: "Render a close-up of a specific region of the product (collar, cuff, hem, print, etc.). Requires an approved Ghost Mannequin. region_slug is a detail-region preset slug. Returns image_job_id.",
|
|
334
|
+
inputSchema: {
|
|
335
|
+
product_id: productIdField,
|
|
336
|
+
region_slug: z.string().min(1).describe('Detail-region preset slug (e.g. "collar", "cuff").')
|
|
337
|
+
},
|
|
338
|
+
outputSchema: jobAcceptedShape,
|
|
339
|
+
annotations: { title: "Generate Detail close-up", ...WRITE },
|
|
340
|
+
resolve: (input) => ({
|
|
341
|
+
mcpName: "generate_derivation",
|
|
342
|
+
args: {
|
|
343
|
+
product_id: toNumericProductId(input.product_id),
|
|
344
|
+
derivation: "detail",
|
|
345
|
+
region_slug: input.region_slug
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
},
|
|
349
|
+
// 4. Lifestyle
|
|
350
|
+
{
|
|
351
|
+
name: "enhancer_lifestyle",
|
|
352
|
+
title: "Generate Lifestyle scene",
|
|
353
|
+
description: "Render the product worn by a model in a lifestyle scene (cafe, street, at home, \u2026). Requires an approved Ghost Mannequin. Needs model_id, pose_id, and a lifestyle scene_slug. Returns image_job_id.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
product_id: productIdField,
|
|
356
|
+
model_id: z.string().min(1).describe("Curated model profile id."),
|
|
357
|
+
pose_id: z.string().min(1).describe("Pose preset id."),
|
|
358
|
+
scene_slug: z.string().min(1).describe("Lifestyle scene preset slug.")
|
|
359
|
+
},
|
|
360
|
+
outputSchema: jobAcceptedShape,
|
|
361
|
+
annotations: { title: "Generate Lifestyle scene", ...WRITE },
|
|
362
|
+
resolve: (input) => ({
|
|
363
|
+
mcpName: "generate_derivation",
|
|
364
|
+
args: {
|
|
365
|
+
product_id: toNumericProductId(input.product_id),
|
|
366
|
+
derivation: "lifestyle",
|
|
367
|
+
model_id: input.model_id,
|
|
368
|
+
pose_id: input.pose_id,
|
|
369
|
+
scene_slug: input.scene_slug
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
// 5. Flat lay
|
|
374
|
+
{
|
|
375
|
+
name: "enhancer_flat_lay",
|
|
376
|
+
title: "Generate Flat Lay",
|
|
377
|
+
description: "Render the product as a top-down flat lay in a curated scene. Requires an approved Ghost Mannequin. scene_slug is a flat-lay scene preset slug. Returns image_job_id.",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
product_id: productIdField,
|
|
380
|
+
scene_slug: z.string().min(1).describe("Flat-lay scene preset slug.")
|
|
381
|
+
},
|
|
382
|
+
outputSchema: jobAcceptedShape,
|
|
383
|
+
annotations: { title: "Generate Flat Lay", ...WRITE },
|
|
384
|
+
resolve: (input) => ({
|
|
385
|
+
mcpName: "generate_derivation",
|
|
386
|
+
args: {
|
|
387
|
+
product_id: toNumericProductId(input.product_id),
|
|
388
|
+
derivation: "flat_lay",
|
|
389
|
+
scene_slug: input.scene_slug
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
},
|
|
393
|
+
// 6. AI Model Premium
|
|
394
|
+
{
|
|
395
|
+
name: "enhancer_premium_model",
|
|
396
|
+
title: "Generate AI Model Premium",
|
|
397
|
+
description: "Editorial model shot: model + pose + a curated premium scene OR a free-form scene description. Requires an approved Ghost Mannequin (resolved automatically). Provide scene_slug OR refined_prompt (at least one). Returns image_job_id.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
product_id: productIdField,
|
|
400
|
+
model_id: z.string().min(1).describe("Curated model profile id."),
|
|
401
|
+
pose_id: z.string().min(1).describe("Pose preset id."),
|
|
402
|
+
scene_slug: z.string().min(1).optional().describe("Premium scene preset slug. Provide this OR refined_prompt."),
|
|
403
|
+
refined_prompt: z.string().min(20).optional().describe(
|
|
404
|
+
"Free-form editorial scene description (\u226520 chars). Provide this OR scene_slug."
|
|
405
|
+
),
|
|
406
|
+
framing: z.enum(["auto", "full_body", "upper_body", "head_to_knees", "close_up"]).optional().describe("Framing of the shot (default auto).")
|
|
407
|
+
},
|
|
408
|
+
outputSchema: jobAcceptedShape,
|
|
409
|
+
annotations: { title: "Generate AI Model Premium", ...WRITE },
|
|
410
|
+
resolve: (input) => {
|
|
411
|
+
const sceneSlug = input.scene_slug;
|
|
412
|
+
const refinedPrompt = input.refined_prompt;
|
|
413
|
+
if (!sceneSlug && !refinedPrompt) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
"enhancer_premium_model needs either scene_slug (a curated premium scene) or refined_prompt (a free-form scene description, \u226520 chars)."
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
mcpName: "generate_ai_model_premium",
|
|
420
|
+
args: {
|
|
421
|
+
product_id: toNumericProductId(input.product_id),
|
|
422
|
+
model_id: input.model_id,
|
|
423
|
+
pose_id: input.pose_id,
|
|
424
|
+
scene_slug: sceneSlug,
|
|
425
|
+
refined_prompt: refinedPrompt,
|
|
426
|
+
framing: input.framing
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
// 7. Video
|
|
432
|
+
{
|
|
433
|
+
name: "enhancer_video",
|
|
434
|
+
title: "Generate product Video",
|
|
435
|
+
description: "Animate an approved still of the product into a short marketing video. By default uses the approved Ghost Mannequin as the first frame; pass source_version_id to start from another approved still, and final_version_id to interpolate to an end frame (e.g. front\u2192back rotation). Returns image_job_id.",
|
|
436
|
+
inputSchema: {
|
|
437
|
+
product_id: productIdField,
|
|
438
|
+
prompt: z.string().min(20).max(2e3).describe('Motion description (\u226520 chars), e.g. "slow 360\xB0 turn, soft studio light".'),
|
|
439
|
+
duration_seconds: z.union([z.literal(5), z.literal(8), z.literal(10)]).optional().describe("Clip length in seconds: 5, 8, or 10 (default 5)."),
|
|
440
|
+
resolution: z.enum(["720p", "1080p"]).optional().describe("Output resolution (default 720p)."),
|
|
441
|
+
engine: z.enum(["seedance", "kling"]).optional().describe("Video engine A/B (default seedance)."),
|
|
442
|
+
source_version_id: z.string().min(1).optional().describe(
|
|
443
|
+
"Approved still version to use as the first frame. Defaults to the Ghost Mannequin."
|
|
444
|
+
),
|
|
445
|
+
final_version_id: z.string().min(1).optional().describe("Optional approved still to interpolate to as the end frame.")
|
|
446
|
+
},
|
|
447
|
+
outputSchema: jobAcceptedShape,
|
|
448
|
+
annotations: { title: "Generate product Video", ...WRITE },
|
|
449
|
+
resolve: (input) => ({
|
|
450
|
+
mcpName: "generate_video",
|
|
451
|
+
args: {
|
|
452
|
+
product_id: toNumericProductId(input.product_id),
|
|
453
|
+
prompt: input.prompt,
|
|
454
|
+
duration_seconds: input.duration_seconds,
|
|
455
|
+
resolution: input.resolution,
|
|
456
|
+
engine: input.engine,
|
|
457
|
+
source_version_id: input.source_version_id,
|
|
458
|
+
final_version_id: input.final_version_id
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
},
|
|
462
|
+
// 8. Banner
|
|
463
|
+
{
|
|
464
|
+
name: "enhancer_banner",
|
|
465
|
+
title: "Generate Banner (outpaint)",
|
|
466
|
+
description: "Outpaint an approved still of the product to a wide / tall banner aspect ratio (16:9, 4:1, or 9:16). Defaults to the approved Ghost Mannequin; pass source_version_id to expand a different approved still. Returns image_job_id.",
|
|
467
|
+
inputSchema: {
|
|
468
|
+
product_id: productIdField,
|
|
469
|
+
aspect_ratio: z.enum(["16:9", "4:1", "9:16"]).describe("Target banner aspect ratio."),
|
|
470
|
+
source_version_id: z.string().min(1).optional().describe("Approved still version to expand. Defaults to the Ghost Mannequin.")
|
|
471
|
+
},
|
|
472
|
+
outputSchema: jobAcceptedShape,
|
|
473
|
+
annotations: { title: "Generate Banner (outpaint)", ...WRITE },
|
|
474
|
+
resolve: (input) => ({
|
|
475
|
+
mcpName: "generate_banner",
|
|
476
|
+
args: {
|
|
477
|
+
product_id: toNumericProductId(input.product_id),
|
|
478
|
+
aspect_ratio: input.aspect_ratio,
|
|
479
|
+
source_version_id: input.source_version_id
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
},
|
|
483
|
+
// 9. Iterate
|
|
484
|
+
{
|
|
485
|
+
name: "enhancer_iterate",
|
|
486
|
+
title: "Iterate on a result",
|
|
487
|
+
description: "Re-run an existing image job with a text adjustment (and optionally a marked-up screenshot showing exactly what to change). Creates a new image_job that inherits the source + spec and applies the adjustment. Returns the new image_job_id.",
|
|
488
|
+
inputSchema: {
|
|
489
|
+
image_job_id: z.string().min(1).describe("The image_job_id to iterate on (from a previous generation)."),
|
|
490
|
+
adjustment: z.string().min(1).max(2e3).describe(
|
|
491
|
+
'What to change, in plain language, e.g. "make the background warmer, keep the logo crisp".'
|
|
492
|
+
),
|
|
493
|
+
annotation_image_url: z.string().url().optional().describe(
|
|
494
|
+
"Optional https URL of a marked-up screenshot (arrows/circles) indicating the exact edits."
|
|
495
|
+
)
|
|
496
|
+
},
|
|
497
|
+
outputSchema: jobAcceptedShape,
|
|
498
|
+
annotations: { title: "Iterate on a result", ...WRITE },
|
|
499
|
+
resolve: (input) => ({
|
|
500
|
+
mcpName: "iterate_version",
|
|
501
|
+
args: {
|
|
502
|
+
image_job_id: input.image_job_id,
|
|
503
|
+
adjustment: input.adjustment,
|
|
504
|
+
annotation_image_url: input.annotation_image_url
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
},
|
|
508
|
+
// 10. Job status
|
|
509
|
+
{
|
|
510
|
+
name: "enhancer_job_status",
|
|
511
|
+
title: "Check job status",
|
|
512
|
+
description: "Poll the status of an image_job created by any enhancer_* generation tool. Returns the current phase (extract / generate / validate / publish), the status, and the latest version id when generation completed. Typical end-to-end duration is 30\u201390s.",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
image_job_id: z.string().min(1).describe("The image_job_id to poll.")
|
|
515
|
+
},
|
|
516
|
+
outputSchema: jobStatusShape,
|
|
517
|
+
annotations: { title: "Check job status", ...READ },
|
|
518
|
+
resolve: (input) => ({
|
|
519
|
+
mcpName: "get_job_status",
|
|
520
|
+
args: { image_job_id: input.image_job_id }
|
|
521
|
+
})
|
|
522
|
+
},
|
|
523
|
+
// 11. List products by status
|
|
524
|
+
{
|
|
525
|
+
name: "enhancer_list_products",
|
|
526
|
+
title: "List products by derivation status",
|
|
527
|
+
description: "List the merchant's products filtered by Image Enhancer derivation status (no_gm | gm_ready | partial | complete | all), optionally narrowing to those missing a specific derivation.",
|
|
528
|
+
inputSchema: {
|
|
529
|
+
status: z.enum(["no_gm", "gm_ready", "partial", "complete", "all"]).optional().describe("Filter by overall derivation status (default all)."),
|
|
530
|
+
missing_mode: z.enum(["ai_model", "detail", "lifestyle", "flat_lay"]).optional().describe("Only products missing this derivation."),
|
|
531
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 20).")
|
|
532
|
+
},
|
|
533
|
+
outputSchema: productListShape,
|
|
534
|
+
annotations: { title: "List products by derivation status", ...READ },
|
|
535
|
+
resolve: (input) => ({
|
|
536
|
+
mcpName: "list_products_by_status",
|
|
537
|
+
args: {
|
|
538
|
+
status: input.status,
|
|
539
|
+
missing_mode: input.missing_mode,
|
|
540
|
+
limit: input.limit
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
},
|
|
544
|
+
// 12. Product studio status
|
|
545
|
+
{
|
|
546
|
+
name: "enhancer_product_status",
|
|
547
|
+
title: "Get product studio status",
|
|
548
|
+
description: "Get the full derivation status for one product (which of ghost_mannequin / ai_model / detail / lifestyle / flat_lay are approved).",
|
|
549
|
+
inputSchema: {
|
|
550
|
+
product_id: productIdField
|
|
551
|
+
},
|
|
552
|
+
outputSchema: productStatusShape,
|
|
553
|
+
annotations: { title: "Get product studio status", ...READ },
|
|
554
|
+
resolve: (input) => ({
|
|
555
|
+
mcpName: "get_product_studio_status",
|
|
556
|
+
args: { product_id: toNumericProductId(input.product_id) }
|
|
557
|
+
})
|
|
558
|
+
},
|
|
559
|
+
// 13. List pending approvals
|
|
560
|
+
{
|
|
561
|
+
name: "enhancer_list_pending",
|
|
562
|
+
title: "List pending approvals",
|
|
563
|
+
description: "List generated image versions waiting for merchant approval, optionally filtered by mode.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
mode: z.enum(["ghost_mannequin", "ai_model", "detail", "lifestyle", "flat_lay"]).optional().describe("Only versions of this mode."),
|
|
566
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 20).")
|
|
567
|
+
},
|
|
568
|
+
outputSchema: pendingListShape,
|
|
569
|
+
annotations: { title: "List pending approvals", ...READ },
|
|
570
|
+
resolve: (input) => ({
|
|
571
|
+
mcpName: "list_pending_approvals",
|
|
572
|
+
args: { mode: input.mode, limit: input.limit }
|
|
573
|
+
})
|
|
574
|
+
},
|
|
575
|
+
// 14. Approve
|
|
576
|
+
{
|
|
577
|
+
name: "enhancer_approve",
|
|
578
|
+
title: "Approve a version",
|
|
579
|
+
description: "Approve a pending image version. It transitions to pending_publish and the publish phase pushes it to the Shopify CDN as a product image.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
version_id: z.string().min(1).describe("The image version id to approve.")
|
|
582
|
+
},
|
|
583
|
+
outputSchema: approveShape,
|
|
584
|
+
annotations: { title: "Approve a version", ...STATE_CHANGE },
|
|
585
|
+
resolve: (input) => ({
|
|
586
|
+
mcpName: "approve_version",
|
|
587
|
+
args: { version_id: input.version_id }
|
|
588
|
+
})
|
|
589
|
+
},
|
|
590
|
+
// 15. Reject
|
|
591
|
+
{
|
|
592
|
+
name: "enhancer_reject",
|
|
593
|
+
title: "Reject a version",
|
|
594
|
+
description: "Reject a pending image version. It transitions to rejected; you can regenerate with enhancer_iterate.",
|
|
595
|
+
inputSchema: {
|
|
596
|
+
version_id: z.string().min(1).describe("The image version id to reject."),
|
|
597
|
+
reason: z.string().optional().describe("Optional rejection reason (audit / retry hint).")
|
|
598
|
+
},
|
|
599
|
+
outputSchema: approveShape,
|
|
600
|
+
annotations: { title: "Reject a version", ...STATE_CHANGE },
|
|
601
|
+
resolve: (input) => ({
|
|
602
|
+
mcpName: "reject_version",
|
|
603
|
+
args: { version_id: input.version_id, reason: input.reason }
|
|
604
|
+
})
|
|
605
|
+
},
|
|
606
|
+
// 16. Feedback submit (carita)
|
|
607
|
+
{
|
|
608
|
+
name: "enhancer_feedback_submit",
|
|
609
|
+
title: "Rate a result (carita)",
|
|
610
|
+
description: "Rate a generated result with a carita \u{1F61E}\u{1F610}\u{1F642}\u{1F604} \u2192 1..4 (1 worst, 4 best), with an optional comment. Re-rating the same version updates the score; low caritas feed the improvement points in enhancer_feedback_report.",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
version_id: z.string().min(1).describe("The image version id being rated."),
|
|
613
|
+
rating: z.number().int().min(1).max(4).describe("1=\u{1F61E} (worst) \xB7 2=\u{1F610} \xB7 3=\u{1F642} \xB7 4=\u{1F604} (best)."),
|
|
614
|
+
comment: z.string().optional().describe("Optional note on what to improve.")
|
|
615
|
+
},
|
|
616
|
+
outputSchema: {
|
|
617
|
+
ok: z.boolean().optional(),
|
|
618
|
+
version_id: z.string().optional(),
|
|
619
|
+
rating: z.number().optional(),
|
|
620
|
+
comment: z.string().nullable().optional()
|
|
621
|
+
},
|
|
622
|
+
annotations: { title: "Rate a result (carita)", ...STATE_CHANGE },
|
|
623
|
+
resolve: (input) => ({
|
|
624
|
+
mcpName: "feedback_submit",
|
|
625
|
+
args: { version_id: input.version_id, rating: input.rating, comment: input.comment }
|
|
626
|
+
})
|
|
627
|
+
},
|
|
628
|
+
// 17. Feedback report
|
|
629
|
+
{
|
|
630
|
+
name: "enhancer_feedback_report",
|
|
631
|
+
title: "Feedback report",
|
|
632
|
+
description: "Aggregate your result ratings: total, average, distribution per carita, per-mode averages (worst first), and the improvement points (low-rated results with comments).",
|
|
633
|
+
inputSchema: {
|
|
634
|
+
window_days: z.number().int().min(1).max(365).optional().describe("Only ratings from the last N days (default: all time).")
|
|
635
|
+
},
|
|
636
|
+
outputSchema: {
|
|
637
|
+
total: z.number().optional(),
|
|
638
|
+
average: z.number().nullable().optional(),
|
|
639
|
+
distribution: z.record(z.unknown()).optional(),
|
|
640
|
+
by_mode: z.array(z.record(z.unknown())).optional(),
|
|
641
|
+
improvement_points: z.array(z.record(z.unknown())).optional()
|
|
642
|
+
},
|
|
643
|
+
annotations: { title: "Feedback report", ...READ },
|
|
644
|
+
resolve: (input) => ({
|
|
645
|
+
mcpName: "feedback_report",
|
|
646
|
+
args: { window_days: input.window_days }
|
|
647
|
+
})
|
|
648
|
+
},
|
|
649
|
+
// 18. Result iterate (feedback-driven re-run)
|
|
650
|
+
{
|
|
651
|
+
name: "enhancer_result_iterate",
|
|
652
|
+
title: "Iterate a result with new params",
|
|
653
|
+
description: "Re-run a result with new params after a low rating: spawns a fresh attempt from the result's job, optionally with a text adjustment, a different model/pose, and/or a style reference. Returns the new image_job_id.",
|
|
654
|
+
inputSchema: {
|
|
655
|
+
image_job_id: z.string().min(1).describe("The image_job_id of the result to iterate on."),
|
|
656
|
+
adjustment: z.string().max(2e3).optional().describe("Optional plain-language change (\u22642000 chars)."),
|
|
657
|
+
model_id: z.string().min(1).optional().describe("Optional different model profile id."),
|
|
658
|
+
pose_id: z.string().min(1).optional().describe("Optional different pose preset id."),
|
|
659
|
+
style_reference_url: z.string().url().optional().describe('Optional "make it look like this" reference image.')
|
|
660
|
+
},
|
|
661
|
+
outputSchema: jobAcceptedShape,
|
|
662
|
+
annotations: { title: "Iterate a result with new params", ...WRITE },
|
|
663
|
+
resolve: (input) => ({
|
|
664
|
+
mcpName: "result_iterate",
|
|
665
|
+
args: {
|
|
666
|
+
image_job_id: input.image_job_id,
|
|
667
|
+
adjustment: input.adjustment,
|
|
668
|
+
model_id: input.model_id,
|
|
669
|
+
pose_id: input.pose_id,
|
|
670
|
+
style_reference_url: input.style_reference_url
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/server.ts
|
|
678
|
+
var SERVER_INFO = { name: "image-enhancer", version: "0.1.0" };
|
|
679
|
+
function createServer(client) {
|
|
680
|
+
const server = new McpServer(SERVER_INFO, {
|
|
681
|
+
instructions: "Image Enhancer \u2014 generate Shopify-ready apparel photography (Ghost Mannequin, AI model, detail, lifestyle, flat lay, premium, video, banner) with strict product-identity preservation. Start with enhancer_ghost_mannequin on a product, poll enhancer_job_status, then derive other shots from the approved Ghost Mannequin. Use enhancer_iterate to refine a result."
|
|
682
|
+
});
|
|
683
|
+
for (const tool of buildTools()) {
|
|
684
|
+
server.registerTool(
|
|
685
|
+
tool.name,
|
|
686
|
+
{
|
|
687
|
+
title: tool.title,
|
|
688
|
+
description: tool.description,
|
|
689
|
+
inputSchema: tool.inputSchema,
|
|
690
|
+
outputSchema: tool.outputSchema,
|
|
691
|
+
annotations: tool.annotations
|
|
692
|
+
},
|
|
693
|
+
async (args) => {
|
|
694
|
+
try {
|
|
695
|
+
const { mcpName, args: mcpArgs } = tool.resolve(args ?? {});
|
|
696
|
+
const data = await client.callTool(mcpName, mcpArgs);
|
|
697
|
+
return {
|
|
698
|
+
structuredContent: data,
|
|
699
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
700
|
+
};
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
703
|
+
return {
|
|
704
|
+
isError: true,
|
|
705
|
+
content: [{ type: "text", text: message }]
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return server;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/index.ts
|
|
715
|
+
async function main() {
|
|
716
|
+
const config = resolveConfig();
|
|
717
|
+
const client = new HttpBridgeClient(config);
|
|
718
|
+
const server = createServer(client);
|
|
719
|
+
const transport = new StdioServerTransport();
|
|
720
|
+
await server.connect(transport);
|
|
721
|
+
process.stderr.write("[image-enhancer-mcp] connected over stdio\n");
|
|
722
|
+
}
|
|
723
|
+
main().catch((err) => {
|
|
724
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
725
|
+
process.stderr.write(`[image-enhancer-mcp] fatal: ${message}
|
|
726
|
+
`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-enhancer-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "stdio MCP server — the merchant's Claude (Desktop + Code) talks to Image Enhancer through this. Bridges stdio JSON-RPC to the app's HTTP /mcp endpoint with the merchant's ie_ token.",
|
|
6
|
+
"keywords": ["mcp", "model-context-protocol", "shopify", "image-enhancer", "claude"],
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"bin": {
|
|
9
|
+
"image-enhancer-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": ["dist"],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"prepack": "tsup",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"dev": "tsx watch src/index.ts",
|
|
20
|
+
"start": "tsx src/index.ts",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
25
|
+
"zod": "^3.23.8"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.7.5",
|
|
29
|
+
"tsup": "8.5.1",
|
|
30
|
+
"tsx": "^4.21.0",
|
|
31
|
+
"typescript": "^5.6.3",
|
|
32
|
+
"vitest": "^2.1.3"
|
|
33
|
+
}
|
|
34
|
+
}
|