mcp-relight-harmonize 1.0.1 → 1.0.3
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/.agents/skills/mcp-relight-harmonize/SKILL.md +22 -0
- package/.agents/skills/mcp-relight-harmonize/scripts/extract_layers.py +397 -0
- package/MASTER.md +162 -89
- package/README.md +4 -2
- package/dist/domain/layer_extractor.d.ts +42 -0
- package/dist/domain/layer_extractor.js +350 -0
- package/dist/index.js +2 -2
- package/dist/resources/presets.js +1 -1
- package/dist/server.js +302 -21
- package/dist/tools/analyze_optical.d.ts +1 -1
- package/dist/tools/analyze_optical.js +25 -6
- package/dist/tools/list_cache.d.ts +9 -0
- package/dist/tools/list_cache.js +68 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -7,11 +7,46 @@ const analyze_optical_1 = require("./tools/analyze_optical");
|
|
|
7
7
|
const generate_relight_1 = require("./tools/generate_relight");
|
|
8
8
|
const harmonize_1 = require("./tools/harmonize");
|
|
9
9
|
const synthesize_prompt_1 = require("./tools/synthesize_prompt");
|
|
10
|
+
const list_cache_1 = require("./tools/list_cache");
|
|
10
11
|
const presets_1 = require("./resources/presets");
|
|
12
|
+
// Common output envelope schema definition
|
|
13
|
+
const StandardEnvelopeSchema = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
status: { type: "string", enum: ["success", "partial", "blocked", "failed"], description: "Execution status." },
|
|
17
|
+
summary: { type: "string", description: "Human-readable executive summary of the operation." },
|
|
18
|
+
data: { type: "object", description: "Typed domain payload." },
|
|
19
|
+
warnings: { type: "array", items: { type: "string" }, description: "Non-fatal warnings if applicable." },
|
|
20
|
+
evidence: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
inputsDigest: { type: "string", description: "SHA-256 digest of input parameters." },
|
|
24
|
+
sources: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: { label: { type: "string" }, uri: { type: "string" } },
|
|
29
|
+
required: ["label"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
artifacts: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { label: { type: "string" }, uri: { type: "string" } },
|
|
37
|
+
required: ["label"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
nextActions: { type: "array", items: { type: "string" }, description: "Actionable follow-up guidance." },
|
|
43
|
+
},
|
|
44
|
+
required: ["status", "summary", "data", "warnings", "evidence", "nextActions"],
|
|
45
|
+
};
|
|
11
46
|
function createServer() {
|
|
12
47
|
const server = new index_js_1.Server({
|
|
13
48
|
name: "mcp-relight-harmonize",
|
|
14
|
-
version: "1.0.
|
|
49
|
+
version: "1.0.3",
|
|
15
50
|
}, {
|
|
16
51
|
capabilities: {
|
|
17
52
|
tools: {},
|
|
@@ -24,94 +59,332 @@ function createServer() {
|
|
|
24
59
|
tools: [
|
|
25
60
|
{
|
|
26
61
|
name: "analyze_optical_profile",
|
|
27
|
-
description: "Extract physical optical metrics from an image
|
|
28
|
-
"dominant
|
|
62
|
+
description: "Extract physical optical metrics from an image, including Correlated Color Temperature (CCT in Kelvin), " +
|
|
63
|
+
"dominant 3D lighting vector (azimuth and elevation angles), photometric luminance dynamic range, contrast zones, " +
|
|
64
|
+
"surface normal roughness index, and optionally decompose into 6 analytical image layers in Layers/ directory.\n\n" +
|
|
65
|
+
"• Purpose: Diagnostic optical extraction and 6-layer decomposition (Highlights, Shadows, Ambient Occlusion, Edges, Depth Normals, Chroma/Saturation). " +
|
|
66
|
+
"Unlike 'generate_relight_variations' which creates artistic relit renders, this tool extracts physical diagnostic metrics and visual analytical decomposition layers.\n" +
|
|
67
|
+
"• Behavior: Read-only by default; writes 6 analytical layer PNGs and Layer.md to the specified directory when 'extract_layers' is true. Unmetered local execution, zero network egress, zero external auth.\n" +
|
|
68
|
+
"• When to use: Use as the prerequisite first step to inspect baseline lighting conditions or generate the 6 diagnostic layers into Layers/ before relighting or prompting.\n" +
|
|
69
|
+
"• When NOT to use: Do NOT use if you need creative relighted styles (use 'generate_relight_variations'), if merging a cutout into a scene " +
|
|
70
|
+
"(use 'harmonize_composite'), or if you only need generative AI prompt synthesis (use 'synthesize_diffusion_prompt').\n" +
|
|
71
|
+
"• Alternatives: Use 'generate_relight_variations' for creative lighting styles, or 'synthesize_diffusion_prompt' for model prompts.",
|
|
29
72
|
inputSchema: {
|
|
30
73
|
type: "object",
|
|
31
74
|
properties: {
|
|
32
75
|
image_path: {
|
|
33
76
|
type: "string",
|
|
34
|
-
description: "Absolute or workspace-relative path to
|
|
77
|
+
description: "Absolute or workspace-relative path to a local image file (.png, .jpg, or .jpeg). " +
|
|
78
|
+
"Must be an existing image under 50 MB.",
|
|
79
|
+
},
|
|
80
|
+
extract_layers: {
|
|
81
|
+
type: "boolean",
|
|
82
|
+
description: "If true, decomposes image into 6 analytical layers (Highlights, Shadows, Ambient Occlusion, Edges, Depth Normals, Chroma/Saturation) saved to Layers/ directory and generates Layer.md.",
|
|
83
|
+
},
|
|
84
|
+
layers_dir: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Optional target directory to store the 6 analytical layer images and Layer.md (defaults to 'Layers').",
|
|
87
|
+
},
|
|
88
|
+
user_intent: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional creative or corrective intent to dynamically tailor layer diagnostics, recommendations, and diffusion prompts.",
|
|
35
91
|
},
|
|
36
92
|
},
|
|
37
93
|
required: ["image_path"],
|
|
38
94
|
},
|
|
95
|
+
outputSchema: {
|
|
96
|
+
...StandardEnvelopeSchema,
|
|
97
|
+
properties: {
|
|
98
|
+
...StandardEnvelopeSchema.properties,
|
|
99
|
+
data: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
imagePath: { type: "string", description: "Canonical resolved file path." },
|
|
103
|
+
dimensions: { type: "array", items: { type: "number" }, description: "[width, height] in pixels." },
|
|
104
|
+
colorTemperatureKelvin: { type: "number", description: "Correlated Color Temperature (CCT) in Kelvin." },
|
|
105
|
+
dominantLightDirectionVector: { type: "array", items: { type: "number" }, description: "Normalized [X, Y, Z] vector." },
|
|
106
|
+
lightingAngles: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
azimuthDeg: { type: "number", description: "Horizontal angle (0-360°)." },
|
|
110
|
+
elevationDeg: { type: "number", description: "Vertical elevation angle (0-90°)." },
|
|
111
|
+
},
|
|
112
|
+
required: ["azimuthDeg", "elevationDeg"],
|
|
113
|
+
},
|
|
114
|
+
meanLuminance: { type: "number", description: "Average photometric luminance (0-255)." },
|
|
115
|
+
luminanceDynamics: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
min: { type: "number" },
|
|
119
|
+
max: { type: "number" },
|
|
120
|
+
p5: { type: "number" },
|
|
121
|
+
median: { type: "number" },
|
|
122
|
+
p95: { type: "number" },
|
|
123
|
+
contrastRatio: { type: "number" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
contrastZones: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
specularHighlightsPct: { type: "number" },
|
|
130
|
+
deepShadowsPct: { type: "number" },
|
|
131
|
+
midtonesPct: { type: "number" },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
surfaceNormalVariation: { type: "number", description: "Roughness metric (std dev of normals)." },
|
|
135
|
+
opticalProfileSummary: { type: "string", description: "Executive summary sentence." },
|
|
136
|
+
},
|
|
137
|
+
required: [
|
|
138
|
+
"imagePath",
|
|
139
|
+
"dimensions",
|
|
140
|
+
"colorTemperatureKelvin",
|
|
141
|
+
"dominantLightDirectionVector",
|
|
142
|
+
"lightingAngles",
|
|
143
|
+
"meanLuminance",
|
|
144
|
+
"luminanceDynamics",
|
|
145
|
+
"contrastZones",
|
|
146
|
+
"surfaceNormalVariation",
|
|
147
|
+
"opticalProfileSummary",
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
39
152
|
},
|
|
40
153
|
{
|
|
41
154
|
name: "generate_relight_variations",
|
|
42
|
-
description: "Generate 4 physically-grounded relit image variations (Ambient fill, Dramatic chiaroscuro, " +
|
|
43
|
-
"Rim light halo, Mood golden-hour)
|
|
155
|
+
description: "Generate 4 physically-grounded relit image variations on disk (Ambient fill, Dramatic chiaroscuro, " +
|
|
156
|
+
"Rim light halo, Mood golden-hour) with mathematical adjustment logs detailing exposure compensation (EV stops) and color balance.\n\n" +
|
|
157
|
+
"• Purpose: Visual image transformation. Unlike 'analyze_optical_profile' which is read-only, this tool renders and writes concrete " +
|
|
158
|
+
"image files to the destination directory. Unlike 'synthesize_diffusion_prompt', it produces immediate local image files.\n" +
|
|
159
|
+
"• Behavior: Mutates filesystem by creating up to 4 image files in the output directory. Deterministic, unmetered local compute, " +
|
|
160
|
+
"no network egress, no authentication required. Re-running overwrites previous variations with the same base name.\n" +
|
|
161
|
+
"• When to use: Use when you need tangible image alternatives of a photo or product render with alternative lighting schemes.\n" +
|
|
162
|
+
"• When NOT to use: Do NOT use if you only need optical metrics (use 'analyze_optical_profile'), if blending a cutout into a background " +
|
|
163
|
+
"(use 'harmonize_composite'), or if you need diffusion AI text prompts (use 'synthesize_diffusion_prompt').\n" +
|
|
164
|
+
"• Alternatives: Use 'synthesize_diffusion_prompt' for text prompts targeting GPT Image or Nano Banana, or 'list_cached_variations' to browse existing outputs.",
|
|
44
165
|
inputSchema: {
|
|
45
166
|
type: "object",
|
|
46
167
|
properties: {
|
|
47
168
|
image_path: {
|
|
48
169
|
type: "string",
|
|
49
|
-
description: "Path to the
|
|
170
|
+
description: "Path to the input image (.png, .jpg, .jpeg) to relight.",
|
|
50
171
|
},
|
|
51
172
|
target_lighting: {
|
|
52
173
|
type: "string",
|
|
53
174
|
enum: ["Ambient", "Dramatic", "Rim", "Mood", "All"],
|
|
54
175
|
default: "All",
|
|
55
|
-
description: "Lighting preset: 'Ambient'
|
|
176
|
+
description: "Lighting preset selection: 'Ambient' (+0.8 EV lifted shadows, 5500K daylight), " +
|
|
177
|
+
"'Dramatic' (-1.5 EV shadow crush, chiaroscuro S-curve), 'Rim' (+1.2 EV normal curvature perimeter halo), " +
|
|
178
|
+
"'Mood' (3200K tungsten amber shift, highlight bloom), or 'All' to generate all four simultaneously. Defaults to 'All'.",
|
|
56
179
|
},
|
|
57
180
|
output_dir: {
|
|
58
181
|
type: "string",
|
|
59
|
-
|
|
182
|
+
default: "",
|
|
183
|
+
description: "Destination folder for generated variation image files. If omitted, defaults to the server's configured cache directory ('./generated_variations').",
|
|
60
184
|
},
|
|
61
185
|
},
|
|
62
186
|
required: ["image_path"],
|
|
63
187
|
},
|
|
188
|
+
outputSchema: {
|
|
189
|
+
...StandardEnvelopeSchema,
|
|
190
|
+
properties: {
|
|
191
|
+
...StandardEnvelopeSchema.properties,
|
|
192
|
+
data: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
originalImage: { type: "string" },
|
|
196
|
+
outputDir: { type: "string" },
|
|
197
|
+
totalVariations: { type: "number" },
|
|
198
|
+
variations: {
|
|
199
|
+
type: "array",
|
|
200
|
+
items: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
presetName: { type: "string" },
|
|
204
|
+
imagePath: { type: "string" },
|
|
205
|
+
fileSizeBytes: { type: "number" },
|
|
206
|
+
evShiftStops: { type: "number" },
|
|
207
|
+
targetCctKelvin: { type: "number" },
|
|
208
|
+
adjustmentsApplied: { type: "array", items: { type: "string" } },
|
|
209
|
+
},
|
|
210
|
+
required: ["presetName", "imagePath", "fileSizeBytes", "evShiftStops", "targetCctKelvin", "adjustmentsApplied"],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ["originalImage", "outputDir", "totalVariations", "variations"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
64
218
|
},
|
|
65
219
|
{
|
|
66
220
|
name: "harmonize_composite",
|
|
67
|
-
description: "Harmonize and composite a foreground
|
|
68
|
-
"statistics transfer,
|
|
221
|
+
description: "Harmonize and composite a foreground subject cutout onto a background environment scene image using Reinhard " +
|
|
222
|
+
"color statistics transfer in decorrelated lαβ space, background color temperature matching, and synthesized ground contact shadows.\n\n" +
|
|
223
|
+
"• Purpose: Two-image compositing and photometric harmonization. Distinct from 'generate_relight_variations' which operates on a single image, " +
|
|
224
|
+
"this tool resolves color mismatches and grounding between two separate image sources.\n" +
|
|
225
|
+
"• Behavior: Mutates filesystem by writing 1 composited PNG image to the output cache directory. Deterministic, local execution, " +
|
|
226
|
+
"no network egress, no authentication required.\n" +
|
|
227
|
+
"• When to use: Use when placing a cutout product, character, or object onto a new background environment scene, ensuring realistic color adaptation and floor shadows.\n" +
|
|
228
|
+
"• When NOT to use: Do NOT use to relight a single standalone image (use 'generate_relight_variations'), or to analyze metrics alone (use 'analyze_optical_profile').\n" +
|
|
229
|
+
"• Alternatives: Use 'generate_relight_variations' to alter lighting on a single image, or 'synthesize_diffusion_prompt' to craft inpainting prompts for seam blending.",
|
|
69
230
|
inputSchema: {
|
|
70
231
|
type: "object",
|
|
71
232
|
properties: {
|
|
72
233
|
foreground_path: {
|
|
73
234
|
type: "string",
|
|
74
|
-
description: "Path to
|
|
235
|
+
description: "Path to foreground subject image. Supports transparent PNG (with alpha channel) or solid background.",
|
|
75
236
|
},
|
|
76
237
|
background_path: {
|
|
77
238
|
type: "string",
|
|
78
|
-
description: "Path to
|
|
239
|
+
description: "Path to target background scene image (.png, .jpg, .jpeg).",
|
|
79
240
|
},
|
|
80
241
|
blend_mode: {
|
|
81
242
|
type: "string",
|
|
82
243
|
enum: ["seamless", "alpha"],
|
|
83
244
|
default: "seamless",
|
|
84
|
-
description: "Blending
|
|
245
|
+
description: "Blending mode: 'seamless' (applies Reinhard color transfer + contact shadow + alpha blend) " +
|
|
246
|
+
"or 'alpha' (standard alpha composite with contact shadow only). Defaults to 'seamless'.",
|
|
85
247
|
},
|
|
86
248
|
},
|
|
87
249
|
required: ["foreground_path", "background_path"],
|
|
88
250
|
},
|
|
251
|
+
outputSchema: {
|
|
252
|
+
...StandardEnvelopeSchema,
|
|
253
|
+
properties: {
|
|
254
|
+
...StandardEnvelopeSchema.properties,
|
|
255
|
+
data: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
compositeImagePath: { type: "string", description: "Absolute path to the rendered composite file on disk." },
|
|
259
|
+
blendMode: { type: "string" },
|
|
260
|
+
foregroundPath: { type: "string" },
|
|
261
|
+
backgroundPath: { type: "string" },
|
|
262
|
+
backgroundCctKelvin: { type: "number", description: "Target background color temperature." },
|
|
263
|
+
luminanceScalingFactor: { type: "number" },
|
|
264
|
+
contactShadowApplied: { type: "boolean", description: "True if contact shadow was synthesized at base." },
|
|
265
|
+
details: { type: "object" },
|
|
266
|
+
},
|
|
267
|
+
required: [
|
|
268
|
+
"compositeImagePath",
|
|
269
|
+
"blendMode",
|
|
270
|
+
"foregroundPath",
|
|
271
|
+
"backgroundPath",
|
|
272
|
+
"backgroundCctKelvin",
|
|
273
|
+
"luminanceScalingFactor",
|
|
274
|
+
"contactShadowApplied",
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
89
279
|
},
|
|
90
280
|
{
|
|
91
281
|
name: "synthesize_diffusion_prompt",
|
|
92
|
-
description: "Synthesize precision enhancement and relighting diffusion prompts
|
|
93
|
-
"
|
|
94
|
-
"
|
|
282
|
+
description: "Synthesize precision enhancement and relighting diffusion prompts based on physical optical analysis of an image, " +
|
|
283
|
+
"tailored specifically for GPT Image (DALL-E 3 / GPT-4o) and Nano Banana. Outputs photorealistic prompts with physical keywords " +
|
|
284
|
+
"(exact Kelvin CCT, 3D light angles, volumetric dust rays, contact shadows) and calibrated denoising parameters (0.35 - 0.45).\n\n" +
|
|
285
|
+
"• Purpose: Generative AI prompt synthesis. Unlike 'generate_relight_variations' which creates image files locally, this tool translates " +
|
|
286
|
+
"optical geometry into targeted text prompts and hyperparameter sets for external diffusion generators.\n" +
|
|
287
|
+
"• Behavior: Completely read-only, deterministic, zero filesystem modifications, no network calls, and no authentication required.\n" +
|
|
288
|
+
"• When to use: Use when you want to feed photorealistic lighting directives or inpainting prompts into GPT Image or Nano Banana.\n" +
|
|
289
|
+
"• When NOT to use: Do NOT use if you need local image rendering without an external AI model (use 'generate_relight_variations'), " +
|
|
290
|
+
"or if merging cutouts locally (use 'harmonize_composite').\n" +
|
|
291
|
+
"• Alternatives: Use 'generate_relight_variations' for instant offline image files, or 'analyze_optical_profile' for raw numerical statistics.",
|
|
95
292
|
inputSchema: {
|
|
96
293
|
type: "object",
|
|
97
294
|
properties: {
|
|
98
295
|
image_path: {
|
|
99
296
|
type: "string",
|
|
100
|
-
description: "Path to the reference image.",
|
|
297
|
+
description: "Path to the local reference image (.png, .jpg, .jpeg) to extract optical geometry from.",
|
|
101
298
|
},
|
|
102
299
|
user_intent: {
|
|
103
300
|
type: "string",
|
|
104
|
-
|
|
301
|
+
default: "",
|
|
302
|
+
description: "Optional creative context or scenario description (e.g., 'golden sunset portrait', 'cyberpunk studio product').",
|
|
105
303
|
},
|
|
106
304
|
target_model: {
|
|
107
305
|
type: "string",
|
|
108
306
|
enum: ["gpt_image", "nano_banana"],
|
|
109
307
|
default: "gpt_image",
|
|
110
|
-
description: "Target engine: 'gpt_image' (
|
|
308
|
+
description: "Target generative engine: 'gpt_image' (outputs natural descriptive studio directives with 85mm prime lens and physical illumination) " +
|
|
309
|
+
"or 'nano_banana' (outputs dense tokenized optical shaders, roughness index, raytraced bounce, and ground contact shadow). Defaults to 'gpt_image'.",
|
|
111
310
|
},
|
|
112
311
|
},
|
|
113
312
|
required: ["image_path"],
|
|
114
313
|
},
|
|
314
|
+
outputSchema: {
|
|
315
|
+
...StandardEnvelopeSchema,
|
|
316
|
+
properties: {
|
|
317
|
+
...StandardEnvelopeSchema.properties,
|
|
318
|
+
data: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
targetModel: { type: "string", enum: ["GPT Image", "Nano Banana"] },
|
|
322
|
+
userIntent: { type: "string" },
|
|
323
|
+
enhancementPrompt: { type: "string", description: "Prompt for micro-surface detail and lens clarity upgrade." },
|
|
324
|
+
relightingPrompt: { type: "string", description: "Prompt for physical relighting with angles, CCT, and contact shadows." },
|
|
325
|
+
recommendedParameters: { type: "object", description: "Calibrated diffusion settings (denoising 0.35-0.45, etc.)." },
|
|
326
|
+
opticalKeywordsUsed: { type: "array", items: { type: "string" } },
|
|
327
|
+
},
|
|
328
|
+
required: [
|
|
329
|
+
"targetModel",
|
|
330
|
+
"userIntent",
|
|
331
|
+
"enhancementPrompt",
|
|
332
|
+
"relightingPrompt",
|
|
333
|
+
"recommendedParameters",
|
|
334
|
+
"opticalKeywordsUsed",
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "list_cached_variations",
|
|
342
|
+
description: "List and inspect all previously generated relight variation images and harmonized composite files stored in the output cache directory.\n\n" +
|
|
343
|
+
"• Purpose: Cache inventory and artifact retrieval. Resolves pipeline completeness by allowing callers to discover, verify, " +
|
|
344
|
+
"and retrieve generated files without manual filesystem traversal.\n" +
|
|
345
|
+
"• Behavior: Completely read-only, queries local filesystem cache directory, no network calls, no authentication required.\n" +
|
|
346
|
+
"• When to use: Use after running 'generate_relight_variations' or 'harmonize_composite' to verify written artifacts, inspect file sizes, " +
|
|
347
|
+
"and obtain exact paths for downstream analysis.\n" +
|
|
348
|
+
"• When NOT to use: Do NOT use to generate new images (use 'generate_relight_variations') or to analyze optical metrics (use 'analyze_optical_profile').\n" +
|
|
349
|
+
"• Alternatives: Use 'generate_relight_variations' to create new images, or 'harmonize_composite' to merge cutouts.",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
cache_dir: {
|
|
354
|
+
type: "string",
|
|
355
|
+
default: "",
|
|
356
|
+
description: "Optional custom directory path to inspect. If omitted, uses server's default cache directory.",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
outputSchema: {
|
|
361
|
+
...StandardEnvelopeSchema,
|
|
362
|
+
properties: {
|
|
363
|
+
...StandardEnvelopeSchema.properties,
|
|
364
|
+
data: {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: {
|
|
367
|
+
cacheDirectory: { type: "string" },
|
|
368
|
+
totalFiles: { type: "number" },
|
|
369
|
+
files: {
|
|
370
|
+
type: "array",
|
|
371
|
+
items: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
filename: { type: "string" },
|
|
375
|
+
path: { type: "string" },
|
|
376
|
+
sizeBytes: { type: "number" },
|
|
377
|
+
createdAt: { type: "string" },
|
|
378
|
+
type: { type: "string", enum: ["relight_variation", "harmonized_composite", "other"] },
|
|
379
|
+
},
|
|
380
|
+
required: ["filename", "path", "sizeBytes", "createdAt", "type"],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
required: ["cacheDirectory", "totalFiles", "files"],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
115
388
|
},
|
|
116
389
|
],
|
|
117
390
|
};
|
|
@@ -123,7 +396,10 @@ function createServer() {
|
|
|
123
396
|
switch (name) {
|
|
124
397
|
case "analyze_optical_profile": {
|
|
125
398
|
const imagePath = String(args.image_path || "");
|
|
126
|
-
|
|
399
|
+
const extractLayers = Boolean(args.extract_layers);
|
|
400
|
+
const layersDir = args.layers_dir ? String(args.layers_dir) : undefined;
|
|
401
|
+
const userIntent = args.user_intent ? String(args.user_intent) : undefined;
|
|
402
|
+
envelope = (0, analyze_optical_1.analyzeOpticalProfileTool)(imagePath, extractLayers, layersDir, userIntent);
|
|
127
403
|
break;
|
|
128
404
|
}
|
|
129
405
|
case "generate_relight_variations": {
|
|
@@ -147,6 +423,11 @@ function createServer() {
|
|
|
147
423
|
envelope = (0, synthesize_prompt_1.synthesizeDiffusionPromptTool)(imagePath, userIntent, targetModel);
|
|
148
424
|
break;
|
|
149
425
|
}
|
|
426
|
+
case "list_cached_variations": {
|
|
427
|
+
const cacheDir = args.cache_dir ? String(args.cache_dir) : undefined;
|
|
428
|
+
envelope = (0, list_cache_1.listCachedVariationsTool)(cacheDir);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
150
431
|
default:
|
|
151
432
|
throw new Error(`Unknown tool: '${name}'`);
|
|
152
433
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ResultEnvelope } from "../core/envelope";
|
|
2
|
-
export declare function analyzeOpticalProfileTool(imagePath: string): ResultEnvelope;
|
|
2
|
+
export declare function analyzeOpticalProfileTool(imagePath: string, extractLayers?: boolean, layersDir?: string, userIntent?: string): ResultEnvelope;
|
|
@@ -8,19 +8,38 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
8
8
|
const envelope_1 = require("../core/envelope");
|
|
9
9
|
const errors_1 = require("../core/errors");
|
|
10
10
|
const optical_analyzer_1 = require("../domain/optical_analyzer");
|
|
11
|
-
|
|
11
|
+
const layer_extractor_1 = require("../domain/layer_extractor");
|
|
12
|
+
function analyzeOpticalProfileTool(imagePath, extractLayers = false, layersDir, userIntent) {
|
|
12
13
|
try {
|
|
13
14
|
const report = (0, optical_analyzer_1.analyzeOpticalProfileImpl)(imagePath);
|
|
14
15
|
const hash = crypto_1.default.createHash("sha256").update(imagePath).digest("hex").slice(0, 12);
|
|
15
|
-
|
|
16
|
+
let layerReport;
|
|
17
|
+
const artifacts = [];
|
|
18
|
+
if (extractLayers || layersDir) {
|
|
19
|
+
const targetDir = layersDir || "Layers";
|
|
20
|
+
layerReport = (0, layer_extractor_1.extractLayersImpl)(imagePath, targetDir, userIntent);
|
|
21
|
+
artifacts.push({ label: "Layer 1 - Highlights", uri: `file://${layerReport.layers.highlights.path}` }, { label: "Layer 2 - Shadows", uri: `file://${layerReport.layers.shadows.path}` }, { label: "Layer 3 - Ambient Occlusion", uri: `file://${layerReport.layers.ambientOcclusion.path}` }, { label: "Layer 4 - Edges", uri: `file://${layerReport.layers.edges.path}` }, { label: "Layer 5 - Depth Normals", uri: `file://${layerReport.layers.depthNormals.path}` }, { label: "Layer 6 - Chroma Saturation", uri: `file://${layerReport.layers.chromaSaturation.path}` }, { label: "Layer Markdown Report", uri: `file://${layerReport.layerMarkdownPath}` });
|
|
22
|
+
}
|
|
23
|
+
const payload = { ...report };
|
|
24
|
+
if (layerReport) {
|
|
25
|
+
payload.layersDirectory = layerReport.layersDirectory;
|
|
26
|
+
payload.layers = layerReport.layers;
|
|
27
|
+
payload.layerMarkdownPath = layerReport.layerMarkdownPath;
|
|
28
|
+
}
|
|
29
|
+
const nextActions = [
|
|
30
|
+
`Call 'generate_relight_variations' on '${imagePath}' to explore alternative lighting schemes.`,
|
|
31
|
+
`Call 'synthesize_diffusion_prompt' targeting GPT Image or Nano Banana for high-res generative inpainting.`,
|
|
32
|
+
];
|
|
33
|
+
if (layerReport) {
|
|
34
|
+
nextActions.unshift(`Inspect 6 analytical layers and Layer.md, then present to user with prompt: 'ماذا تريد من تعديل؟'`);
|
|
35
|
+
}
|
|
36
|
+
return (0, envelope_1.createEnvelope)("success", `Optical profile extracted successfully: ${report.opticalProfileSummary}${layerReport ? ` (6 Analytical layers generated in '${layerReport.layersDirectory}')` : ""}`, payload, {
|
|
16
37
|
evidence: {
|
|
17
38
|
inputsDigest: `sha256:${hash}`,
|
|
18
39
|
sources: [{ label: "Source Image", uri: `file://${report.imagePath}` }],
|
|
40
|
+
artifacts,
|
|
19
41
|
},
|
|
20
|
-
nextActions
|
|
21
|
-
`Call 'generate_relight_variations' on '${imagePath}' to explore alternative lighting schemes.`,
|
|
22
|
-
`Call 'synthesize_diffusion_prompt' targeting GPT Image or Nano Banana for high-res generative inpainting.`,
|
|
23
|
-
],
|
|
42
|
+
nextActions,
|
|
24
43
|
});
|
|
25
44
|
}
|
|
26
45
|
catch (err) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ResultEnvelope } from "../core/envelope";
|
|
2
|
+
export interface CachedItem {
|
|
3
|
+
filename: string;
|
|
4
|
+
path: string;
|
|
5
|
+
sizeBytes: number;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
type: "relight_variation" | "harmonized_composite" | "other";
|
|
8
|
+
}
|
|
9
|
+
export declare function listCachedVariationsTool(cacheDir?: string): ResultEnvelope;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listCachedVariationsTool = listCachedVariationsTool;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const envelope_1 = require("../core/envelope");
|
|
12
|
+
const errors_1 = require("../core/errors");
|
|
13
|
+
function listCachedVariationsTool(cacheDir) {
|
|
14
|
+
try {
|
|
15
|
+
const targetDir = cacheDir ? path_1.default.resolve(cacheDir) : (0, config_1.ensureOutputDirectory)();
|
|
16
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
17
|
+
return (0, envelope_1.createEnvelope)("success", `Output cache directory is empty or does not yet exist: '${targetDir}'`, { cacheDirectory: targetDir, totalFiles: 0, files: [] }, { nextActions: ["Call 'generate_relight_variations' or 'harmonize_composite' to produce variations."] });
|
|
18
|
+
}
|
|
19
|
+
const entries = fs_1.default.readdirSync(targetDir, { withFileTypes: true });
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry.isFile()) {
|
|
23
|
+
const ext = path_1.default.extname(entry.name).toLowerCase();
|
|
24
|
+
if (config_1.config.allowedExtensions.has(ext)) {
|
|
25
|
+
const fullPath = path_1.default.join(targetDir, entry.name);
|
|
26
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
27
|
+
let itemType = "other";
|
|
28
|
+
if (entry.name.includes("_relight_")) {
|
|
29
|
+
itemType = "relight_variation";
|
|
30
|
+
}
|
|
31
|
+
else if (entry.name.startsWith("harmonized_")) {
|
|
32
|
+
itemType = "harmonized_composite";
|
|
33
|
+
}
|
|
34
|
+
files.push({
|
|
35
|
+
filename: entry.name,
|
|
36
|
+
path: fullPath,
|
|
37
|
+
sizeBytes: stat.size,
|
|
38
|
+
createdAt: stat.mtime.toISOString(),
|
|
39
|
+
type: itemType,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Sort by most recent
|
|
45
|
+
files.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
46
|
+
const hash = crypto_1.default.createHash("sha256").update(targetDir).digest("hex").slice(0, 12);
|
|
47
|
+
return (0, envelope_1.createEnvelope)("success", `Found ${files.length} cached variation and composite artifacts in '${targetDir}'.`, { cacheDirectory: targetDir, totalFiles: files.length, files }, {
|
|
48
|
+
evidence: {
|
|
49
|
+
inputsDigest: `sha256:${hash}`,
|
|
50
|
+
artifacts: files.map((f) => ({ label: f.filename, uri: `file://${f.path}` })),
|
|
51
|
+
},
|
|
52
|
+
nextActions: [
|
|
53
|
+
"Call 'analyze_optical_profile' on any cached variation to evaluate its modified optical properties.",
|
|
54
|
+
"Call 'synthesize_diffusion_prompt' to craft AI inpainting prompts from a cached variation.",
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const isAppErr = err instanceof errors_1.AppError;
|
|
60
|
+
const msg = err.message || "Failed to list cached variations.";
|
|
61
|
+
const code = isAppErr ? err.code : "INTERNAL_ERROR";
|
|
62
|
+
const hint = isAppErr ? err.actionableHint : "Verify directory read permissions.";
|
|
63
|
+
return (0, envelope_1.createEnvelope)("failed", `Listing cache failed: ${msg}`, { error_code: code, actionable_hint: hint }, {
|
|
64
|
+
warnings: [msg],
|
|
65
|
+
nextActions: [hint],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-relight-harmonize",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Professional TypeScript Model Context Protocol (MCP) server & agent skill for optical profiling, physical relighting, composite harmonization, and diffusion prompt synthesis (GPT Image & Nano Banana).",
|
|
5
5
|
"author": "MarwanDevSpace <dev@marwandevspace.io>",
|
|
6
6
|
"license": "MIT",
|