awslabs.nova-canvas-mcp-server 0.1.10233__py3-none-any.whl → 0.1.10652__py3-none-any.whl
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.
- awslabs/nova_canvas_mcp_server/__init__.py +1 -1
- awslabs/nova_canvas_mcp_server/consts.py +3 -3
- awslabs/nova_canvas_mcp_server/models.py +27 -23
- awslabs/nova_canvas_mcp_server/novacanvas.py +70 -50
- awslabs/nova_canvas_mcp_server/server.py +76 -56
- {awslabs_nova_canvas_mcp_server-0.1.10233.dist-info → awslabs_nova_canvas_mcp_server-0.1.10652.dist-info}/METADATA +2 -2
- awslabs_nova_canvas_mcp_server-0.1.10652.dist-info/RECORD +10 -0
- awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/RECORD +0 -10
- {awslabs_nova_canvas_mcp_server-0.1.10233.dist-info → awslabs_nova_canvas_mcp_server-0.1.10652.dist-info}/WHEEL +0 -0
- {awslabs_nova_canvas_mcp_server-0.1.10233.dist-info → awslabs_nova_canvas_mcp_server-0.1.10652.dist-info}/entry_points.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Constants
|
|
2
|
-
NOVA_CANVAS_MODEL_ID =
|
|
2
|
+
NOVA_CANVAS_MODEL_ID = "amazon.nova-canvas-v1:0"
|
|
3
3
|
DEFAULT_WIDTH = 1024
|
|
4
4
|
DEFAULT_HEIGHT = 1024
|
|
5
|
-
DEFAULT_QUALITY =
|
|
5
|
+
DEFAULT_QUALITY = "standard"
|
|
6
6
|
DEFAULT_CFG_SCALE = 6.5
|
|
7
7
|
DEFAULT_NUMBER_OF_IMAGES = 1
|
|
8
|
-
DEFAULT_OUTPUT_DIR =
|
|
8
|
+
DEFAULT_OUTPUT_DIR = "output" # Default directory inside workspace_dir
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# Nova Canvas Prompt Best Practices
|
|
@@ -15,8 +15,8 @@ class Quality(str, Enum):
|
|
|
15
15
|
PREMIUM: Premium quality image generation with enhanced details.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
STANDARD =
|
|
19
|
-
PREMIUM =
|
|
18
|
+
STANDARD = "standard"
|
|
19
|
+
PREMIUM = "premium"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class TaskType(str, Enum):
|
|
@@ -27,8 +27,8 @@ class TaskType(str, Enum):
|
|
|
27
27
|
COLOR_GUIDED_GENERATION: Generate an image guided by both text and color palette.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
TEXT_IMAGE =
|
|
31
|
-
COLOR_GUIDED_GENERATION =
|
|
30
|
+
TEXT_IMAGE = "TEXT_IMAGE"
|
|
31
|
+
COLOR_GUIDED_GENERATION = "COLOR_GUIDED_GENERATION"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class ImageGenerationConfig(BaseModel):
|
|
@@ -50,10 +50,12 @@ class ImageGenerationConfig(BaseModel):
|
|
|
50
50
|
height: int = Field(default=1024, ge=320, le=4096)
|
|
51
51
|
quality: Quality = Quality.STANDARD
|
|
52
52
|
cfgScale: float = Field(default=6.5, ge=1.1, le=10.0)
|
|
53
|
-
seed: int = Field(
|
|
53
|
+
seed: int = Field(
|
|
54
|
+
default_factory=lambda: random.randint(0, 858993459), ge=0, le=858993459
|
|
55
|
+
)
|
|
54
56
|
numberOfImages: int = Field(default=1, ge=1, le=5)
|
|
55
57
|
|
|
56
|
-
@field_validator(
|
|
58
|
+
@field_validator("width", "height")
|
|
57
59
|
@classmethod
|
|
58
60
|
def must_be_divisible_by_16(cls, v: int) -> int:
|
|
59
61
|
"""Validate that width and height are divisible by 16.
|
|
@@ -68,10 +70,10 @@ class ImageGenerationConfig(BaseModel):
|
|
|
68
70
|
ValueError: If the value is not divisible by 16.
|
|
69
71
|
"""
|
|
70
72
|
if v % 16 != 0:
|
|
71
|
-
raise ValueError(
|
|
73
|
+
raise ValueError("Value must be divisible by 16")
|
|
72
74
|
return v
|
|
73
75
|
|
|
74
|
-
@model_validator(mode=
|
|
76
|
+
@model_validator(mode="after")
|
|
75
77
|
def validate_aspect_ratio_and_total_pixels(self):
|
|
76
78
|
"""Validate aspect ratio and total pixel count.
|
|
77
79
|
|
|
@@ -91,12 +93,12 @@ class ImageGenerationConfig(BaseModel):
|
|
|
91
93
|
# Check aspect ratio between 1:4 and 4:1
|
|
92
94
|
aspect_ratio = width / height
|
|
93
95
|
if aspect_ratio < 0.25 or aspect_ratio > 4.0:
|
|
94
|
-
raise ValueError(
|
|
96
|
+
raise ValueError("Aspect ratio must be between 1:4 and 4:1")
|
|
95
97
|
|
|
96
98
|
# Check total pixel count
|
|
97
99
|
total_pixels = width * height
|
|
98
100
|
if total_pixels >= 4194304:
|
|
99
|
-
raise ValueError(
|
|
101
|
+
raise ValueError("Total pixel count must be less than 4,194,304")
|
|
100
102
|
|
|
101
103
|
return self
|
|
102
104
|
|
|
@@ -130,7 +132,7 @@ class ColorGuidedGenerationParams(BaseModel):
|
|
|
130
132
|
text: str = Field(..., min_length=1, max_length=1024)
|
|
131
133
|
negativeText: Optional[str] = Field(default=None, min_length=1, max_length=1024)
|
|
132
134
|
|
|
133
|
-
@field_validator(
|
|
135
|
+
@field_validator("colors")
|
|
134
136
|
@classmethod
|
|
135
137
|
def validate_hex_colors(cls, v: List[str]) -> List[str]:
|
|
136
138
|
"""Validate that colors are in the correct hexadecimal format.
|
|
@@ -144,7 +146,7 @@ class ColorGuidedGenerationParams(BaseModel):
|
|
|
144
146
|
Raises:
|
|
145
147
|
ValueError: If any color is not a valid hexadecimal color in the format '#RRGGBB'.
|
|
146
148
|
"""
|
|
147
|
-
hex_pattern = re.compile(r
|
|
149
|
+
hex_pattern = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
148
150
|
for color in v:
|
|
149
151
|
if not hex_pattern.match(color):
|
|
150
152
|
raise ValueError(
|
|
@@ -180,13 +182,13 @@ class TextImageRequest(BaseModel):
|
|
|
180
182
|
"""
|
|
181
183
|
text_to_image_params = self.textToImageParams.model_dump()
|
|
182
184
|
# Remove negativeText if it's None
|
|
183
|
-
if text_to_image_params.get(
|
|
184
|
-
text_to_image_params.pop(
|
|
185
|
+
if text_to_image_params.get("negativeText") is None:
|
|
186
|
+
text_to_image_params.pop("negativeText", None)
|
|
185
187
|
|
|
186
188
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
"taskType": self.taskType,
|
|
190
|
+
"textToImageParams": text_to_image_params,
|
|
191
|
+
"imageGenerationConfig": self.imageGenerationConfig.model_dump()
|
|
190
192
|
if self.imageGenerationConfig
|
|
191
193
|
else None,
|
|
192
194
|
}
|
|
@@ -204,7 +206,9 @@ class ColorGuidedRequest(BaseModel):
|
|
|
204
206
|
imageGenerationConfig: Configuration for image generation.
|
|
205
207
|
"""
|
|
206
208
|
|
|
207
|
-
taskType: Literal[
|
|
209
|
+
taskType: Literal[
|
|
210
|
+
TaskType.COLOR_GUIDED_GENERATION
|
|
211
|
+
] = TaskType.COLOR_GUIDED_GENERATION
|
|
208
212
|
colorGuidedGenerationParams: ColorGuidedGenerationParams
|
|
209
213
|
imageGenerationConfig: Optional[ImageGenerationConfig] = Field(
|
|
210
214
|
default_factory=ImageGenerationConfig
|
|
@@ -219,13 +223,13 @@ class ColorGuidedRequest(BaseModel):
|
|
|
219
223
|
"""
|
|
220
224
|
color_guided_params = self.colorGuidedGenerationParams.model_dump()
|
|
221
225
|
# Remove negativeText if it's None
|
|
222
|
-
if color_guided_params.get(
|
|
223
|
-
color_guided_params.pop(
|
|
226
|
+
if color_guided_params.get("negativeText") is None:
|
|
227
|
+
color_guided_params.pop("negativeText", None)
|
|
224
228
|
|
|
225
229
|
return {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
"taskType": self.taskType,
|
|
231
|
+
"colorGuidedGenerationParams": color_guided_params,
|
|
232
|
+
"imageGenerationConfig": self.imageGenerationConfig.model_dump()
|
|
229
233
|
if self.imageGenerationConfig
|
|
230
234
|
else None,
|
|
231
235
|
}
|
|
@@ -41,7 +41,7 @@ def save_generated_images(
|
|
|
41
41
|
base64_images: List[str],
|
|
42
42
|
filename: Optional[str] = None,
|
|
43
43
|
number_of_images: int = DEFAULT_NUMBER_OF_IMAGES,
|
|
44
|
-
prefix: str =
|
|
44
|
+
prefix: str = "nova_canvas",
|
|
45
45
|
workspace_dir: Optional[str] = None,
|
|
46
46
|
) -> Dict[str, List]:
|
|
47
47
|
"""Save base64-encoded images to files.
|
|
@@ -56,7 +56,7 @@ def save_generated_images(
|
|
|
56
56
|
Returns:
|
|
57
57
|
Dictionary with lists of paths to the saved image files and PIL Image objects.
|
|
58
58
|
"""
|
|
59
|
-
logger.debug(f
|
|
59
|
+
logger.debug(f"Saving {len(base64_images)} images")
|
|
60
60
|
# Determine the output directory
|
|
61
61
|
if workspace_dir:
|
|
62
62
|
output_dir = os.path.join(workspace_dir, DEFAULT_OUTPUT_DIR)
|
|
@@ -73,25 +73,27 @@ def save_generated_images(
|
|
|
73
73
|
# Generate filename if not provided
|
|
74
74
|
if filename:
|
|
75
75
|
image_filename = (
|
|
76
|
-
f
|
|
76
|
+
f"{filename}_{i + 1}.png" if number_of_images > 1 else f"{filename}.png"
|
|
77
77
|
)
|
|
78
78
|
else:
|
|
79
79
|
# Generate a random filename
|
|
80
|
-
random_id =
|
|
81
|
-
|
|
80
|
+
random_id = "".join(
|
|
81
|
+
random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=8)
|
|
82
|
+
)
|
|
83
|
+
image_filename = f"{prefix}_{random_id}_{i + 1}.png"
|
|
82
84
|
|
|
83
85
|
# Decode the base64 image data
|
|
84
86
|
image_data = base64.b64decode(base64_image_data)
|
|
85
87
|
|
|
86
88
|
# Save the image
|
|
87
89
|
image_path = os.path.join(output_dir, image_filename)
|
|
88
|
-
with open(image_path,
|
|
90
|
+
with open(image_path, "wb") as file:
|
|
89
91
|
file.write(image_data)
|
|
90
92
|
# Convert to absolute path
|
|
91
93
|
abs_image_path = os.path.abspath(image_path)
|
|
92
94
|
saved_paths.append(abs_image_path)
|
|
93
95
|
|
|
94
|
-
return {
|
|
96
|
+
return {"paths": saved_paths}
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
async def invoke_nova_canvas(
|
|
@@ -110,22 +112,24 @@ async def invoke_nova_canvas(
|
|
|
110
112
|
Raises:
|
|
111
113
|
Exception: If the API call fails.
|
|
112
114
|
"""
|
|
113
|
-
logger.debug(
|
|
115
|
+
logger.debug("Invoking Nova Canvas API")
|
|
114
116
|
|
|
115
117
|
# Convert the request payload to JSON
|
|
116
118
|
request = json.dumps(request_model_dict)
|
|
117
119
|
|
|
118
120
|
try:
|
|
119
121
|
# Invoke the model
|
|
120
|
-
logger.info(f
|
|
121
|
-
response = bedrock_runtime_client.invoke_model(
|
|
122
|
+
logger.info(f"Sending request to Nova Canvas model: {NOVA_CANVAS_MODEL_ID}")
|
|
123
|
+
response = bedrock_runtime_client.invoke_model(
|
|
124
|
+
modelId=NOVA_CANVAS_MODEL_ID, body=request
|
|
125
|
+
)
|
|
122
126
|
|
|
123
127
|
# Decode the response body
|
|
124
|
-
result = json.loads(response[
|
|
125
|
-
logger.info(
|
|
128
|
+
result = json.loads(response["body"].read())
|
|
129
|
+
logger.info("Nova Canvas API call successful")
|
|
126
130
|
return result
|
|
127
131
|
except Exception as e:
|
|
128
|
-
logger.error(f
|
|
132
|
+
logger.error(f"Nova Canvas API call failed: {str(e)}")
|
|
129
133
|
raise
|
|
130
134
|
|
|
131
135
|
|
|
@@ -165,18 +169,22 @@ async def generate_image_with_text(
|
|
|
165
169
|
ImageGenerationResponse: An object containing the paths to the generated images,
|
|
166
170
|
PIL Image objects, and status information.
|
|
167
171
|
"""
|
|
168
|
-
logger.debug(
|
|
172
|
+
logger.debug(
|
|
173
|
+
f"Generating text-to-image with prompt: '{prompt[:30]}...' ({width}x{height})"
|
|
174
|
+
)
|
|
169
175
|
|
|
170
176
|
try:
|
|
171
177
|
# Validate input parameters using Pydantic
|
|
172
178
|
try:
|
|
173
|
-
logger.debug(
|
|
179
|
+
logger.debug("Validating parameters and creating request model")
|
|
174
180
|
|
|
175
181
|
# Create image generation config
|
|
176
182
|
config = ImageGenerationConfig(
|
|
177
183
|
width=width,
|
|
178
184
|
height=height,
|
|
179
|
-
quality=Quality.STANDARD
|
|
185
|
+
quality=Quality.STANDARD
|
|
186
|
+
if quality == DEFAULT_QUALITY
|
|
187
|
+
else Quality.PREMIUM,
|
|
180
188
|
cfgScale=cfg_scale,
|
|
181
189
|
seed=seed if seed is not None else random.randint(0, 858993459),
|
|
182
190
|
numberOfImages=number_of_images,
|
|
@@ -185,7 +193,9 @@ async def generate_image_with_text(
|
|
|
185
193
|
# Create text-to-image params
|
|
186
194
|
# The Nova Canvas API doesn't accept null for negativeText
|
|
187
195
|
if negative_prompt is not None:
|
|
188
|
-
text_params = TextToImageParams(
|
|
196
|
+
text_params = TextToImageParams(
|
|
197
|
+
text=prompt, negativeText=negative_prompt
|
|
198
|
+
)
|
|
189
199
|
else:
|
|
190
200
|
text_params = TextToImageParams(text=prompt)
|
|
191
201
|
|
|
@@ -196,13 +206,13 @@ async def generate_image_with_text(
|
|
|
196
206
|
|
|
197
207
|
# Convert model to dictionary
|
|
198
208
|
request_model_dict = request_model.to_api_dict()
|
|
199
|
-
logger.info(
|
|
209
|
+
logger.info("Request validation successful")
|
|
200
210
|
|
|
201
211
|
except Exception as e:
|
|
202
|
-
logger.error(f
|
|
212
|
+
logger.error(f"Parameter validation failed: {str(e)}")
|
|
203
213
|
return ImageGenerationResponse(
|
|
204
|
-
status=
|
|
205
|
-
message=f
|
|
214
|
+
status="error",
|
|
215
|
+
message=f"Validation error: {str(e)}",
|
|
206
216
|
paths=[],
|
|
207
217
|
prompt=prompt,
|
|
208
218
|
negative_prompt=negative_prompt,
|
|
@@ -210,34 +220,36 @@ async def generate_image_with_text(
|
|
|
210
220
|
|
|
211
221
|
try:
|
|
212
222
|
# Invoke the Nova Canvas API
|
|
213
|
-
logger.debug(
|
|
214
|
-
model_response = await invoke_nova_canvas(
|
|
223
|
+
logger.debug("Sending request to Nova Canvas API")
|
|
224
|
+
model_response = await invoke_nova_canvas(
|
|
225
|
+
request_model_dict, bedrock_runtime_client
|
|
226
|
+
)
|
|
215
227
|
|
|
216
228
|
# Extract the image data
|
|
217
|
-
base64_images = model_response[
|
|
218
|
-
logger.info(f
|
|
229
|
+
base64_images = model_response["images"]
|
|
230
|
+
logger.info(f"Received {len(base64_images)} images from Nova Canvas API")
|
|
219
231
|
|
|
220
232
|
# Save the generated images
|
|
221
233
|
result = save_generated_images(
|
|
222
234
|
base64_images,
|
|
223
235
|
filename,
|
|
224
236
|
number_of_images,
|
|
225
|
-
prefix=
|
|
237
|
+
prefix="nova_canvas",
|
|
226
238
|
workspace_dir=workspace_dir,
|
|
227
239
|
)
|
|
228
240
|
|
|
229
241
|
logger.info(f'Successfully generated {len(result["paths"])} image(s)')
|
|
230
242
|
return ImageGenerationResponse(
|
|
231
|
-
status=
|
|
243
|
+
status="success",
|
|
232
244
|
message=f'Generated {len(result["paths"])} image(s)',
|
|
233
|
-
paths=result[
|
|
245
|
+
paths=result["paths"],
|
|
234
246
|
prompt=prompt,
|
|
235
247
|
negative_prompt=negative_prompt,
|
|
236
248
|
)
|
|
237
249
|
except Exception as e:
|
|
238
|
-
logger.error(f
|
|
250
|
+
logger.error(f"Image generation failed: {str(e)}")
|
|
239
251
|
return ImageGenerationResponse(
|
|
240
|
-
status=
|
|
252
|
+
status="error",
|
|
241
253
|
message=str(e),
|
|
242
254
|
paths=[],
|
|
243
255
|
prompt=prompt,
|
|
@@ -245,9 +257,9 @@ async def generate_image_with_text(
|
|
|
245
257
|
)
|
|
246
258
|
|
|
247
259
|
except Exception as e:
|
|
248
|
-
logger.error(f
|
|
260
|
+
logger.error(f"Unexpected error in generate_image_with_text: {str(e)}")
|
|
249
261
|
return ImageGenerationResponse(
|
|
250
|
-
status=
|
|
262
|
+
status="error",
|
|
251
263
|
message=str(e),
|
|
252
264
|
paths=[],
|
|
253
265
|
prompt=prompt,
|
|
@@ -300,13 +312,17 @@ async def generate_image_with_colors(
|
|
|
300
312
|
try:
|
|
301
313
|
# Validate input parameters using Pydantic
|
|
302
314
|
try:
|
|
303
|
-
logger.debug(
|
|
315
|
+
logger.debug(
|
|
316
|
+
"Validating parameters and creating color-guided request model"
|
|
317
|
+
)
|
|
304
318
|
|
|
305
319
|
# Create image generation config
|
|
306
320
|
config = ImageGenerationConfig(
|
|
307
321
|
width=width,
|
|
308
322
|
height=height,
|
|
309
|
-
quality=Quality.STANDARD
|
|
323
|
+
quality=Quality.STANDARD
|
|
324
|
+
if quality == DEFAULT_QUALITY
|
|
325
|
+
else Quality.PREMIUM,
|
|
310
326
|
cfgScale=cfg_scale,
|
|
311
327
|
seed=seed if seed is not None else random.randint(0, 858993459),
|
|
312
328
|
numberOfImages=number_of_images,
|
|
@@ -333,13 +349,13 @@ async def generate_image_with_colors(
|
|
|
333
349
|
|
|
334
350
|
# Convert model to dictionary
|
|
335
351
|
request_model_dict = request_model.to_api_dict()
|
|
336
|
-
logger.info(
|
|
352
|
+
logger.info("Color-guided request validation successful")
|
|
337
353
|
|
|
338
354
|
except Exception as e:
|
|
339
|
-
logger.error(f
|
|
355
|
+
logger.error(f"Color-guided parameter validation failed: {str(e)}")
|
|
340
356
|
return ImageGenerationResponse(
|
|
341
|
-
status=
|
|
342
|
-
message=f
|
|
357
|
+
status="error",
|
|
358
|
+
message=f"Validation error: {str(e)}",
|
|
343
359
|
paths=[],
|
|
344
360
|
prompt=prompt,
|
|
345
361
|
negative_prompt=negative_prompt,
|
|
@@ -348,35 +364,39 @@ async def generate_image_with_colors(
|
|
|
348
364
|
|
|
349
365
|
try:
|
|
350
366
|
# Invoke the Nova Canvas API
|
|
351
|
-
logger.debug(
|
|
352
|
-
model_response = await invoke_nova_canvas(
|
|
367
|
+
logger.debug("Sending color-guided request to Nova Canvas API")
|
|
368
|
+
model_response = await invoke_nova_canvas(
|
|
369
|
+
request_model_dict, bedrock_runtime_client
|
|
370
|
+
)
|
|
353
371
|
|
|
354
372
|
# Extract the image data
|
|
355
|
-
base64_images = model_response[
|
|
356
|
-
logger.info(f
|
|
373
|
+
base64_images = model_response["images"]
|
|
374
|
+
logger.info(f"Received {len(base64_images)} images from Nova Canvas API")
|
|
357
375
|
|
|
358
376
|
# Save the generated images
|
|
359
377
|
result = save_generated_images(
|
|
360
378
|
base64_images,
|
|
361
379
|
filename,
|
|
362
380
|
number_of_images,
|
|
363
|
-
prefix=
|
|
381
|
+
prefix="nova_canvas_color",
|
|
364
382
|
workspace_dir=workspace_dir,
|
|
365
383
|
)
|
|
366
384
|
|
|
367
|
-
logger.info(
|
|
385
|
+
logger.info(
|
|
386
|
+
f'Successfully generated {len(result["paths"])} color-guided image(s)'
|
|
387
|
+
)
|
|
368
388
|
return ImageGenerationResponse(
|
|
369
|
-
status=
|
|
389
|
+
status="success",
|
|
370
390
|
message=f'Generated {len(result["paths"])} image(s)',
|
|
371
|
-
paths=result[
|
|
391
|
+
paths=result["paths"],
|
|
372
392
|
prompt=prompt,
|
|
373
393
|
negative_prompt=negative_prompt,
|
|
374
394
|
colors=colors,
|
|
375
395
|
)
|
|
376
396
|
except Exception as e:
|
|
377
|
-
logger.error(f
|
|
397
|
+
logger.error(f"Color-guided image generation failed: {str(e)}")
|
|
378
398
|
return ImageGenerationResponse(
|
|
379
|
-
status=
|
|
399
|
+
status="error",
|
|
380
400
|
message=str(e),
|
|
381
401
|
paths=[],
|
|
382
402
|
prompt=prompt,
|
|
@@ -385,9 +405,9 @@ async def generate_image_with_colors(
|
|
|
385
405
|
)
|
|
386
406
|
|
|
387
407
|
except Exception as e:
|
|
388
|
-
logger.error(f
|
|
408
|
+
logger.error(f"Unexpected error in generate_image_with_colors: {str(e)}")
|
|
389
409
|
return ImageGenerationResponse(
|
|
390
|
-
status=
|
|
410
|
+
status="error",
|
|
391
411
|
message=str(e),
|
|
392
412
|
paths=[],
|
|
393
413
|
prompt=prompt,
|
|
@@ -25,7 +25,7 @@ from typing import TYPE_CHECKING, List, Optional
|
|
|
25
25
|
|
|
26
26
|
# Logging
|
|
27
27
|
logger.remove()
|
|
28
|
-
logger.add(sys.stderr, level=os.getenv(
|
|
28
|
+
logger.add(sys.stderr, level=os.getenv("FASTMCP_LOG_LEVEL", "WARNING"))
|
|
29
29
|
|
|
30
30
|
# Bedrock Runtime Client typing
|
|
31
31
|
if TYPE_CHECKING:
|
|
@@ -36,23 +36,25 @@ else:
|
|
|
36
36
|
|
|
37
37
|
# Bedrock Runtime Client
|
|
38
38
|
bedrock_runtime_client: BedrockRuntimeClient
|
|
39
|
-
aws_region: str = os.environ.get(
|
|
39
|
+
aws_region: str = os.environ.get("AWS_REGION", "us-east-1")
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
|
-
if aws_profile := os.environ.get(
|
|
42
|
+
if aws_profile := os.environ.get("AWS_PROFILE"):
|
|
43
43
|
bedrock_runtime_client = boto3.Session(
|
|
44
44
|
profile_name=aws_profile, region_name=aws_region
|
|
45
|
-
).client(
|
|
45
|
+
).client("bedrock-runtime")
|
|
46
46
|
else:
|
|
47
|
-
bedrock_runtime_client = boto3.Session(region_name=aws_region).client(
|
|
47
|
+
bedrock_runtime_client = boto3.Session(region_name=aws_region).client(
|
|
48
|
+
"bedrock-runtime"
|
|
49
|
+
)
|
|
48
50
|
except Exception as e:
|
|
49
|
-
logger.error(f
|
|
51
|
+
logger.error(f"Error creating bedrock runtime client: {str(e)}")
|
|
50
52
|
raise
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
# Create the MCP server Pwith detailed instructions
|
|
54
56
|
mcp = FastMCP(
|
|
55
|
-
|
|
57
|
+
"awslabs-nova-canvas-mcp-server",
|
|
56
58
|
instructions=f"""
|
|
57
59
|
# Amazon Nova Canvas Image Generation
|
|
58
60
|
|
|
@@ -71,32 +73,33 @@ Generate an image from a text prompt and color palette using Amazon Nova Canvas.
|
|
|
71
73
|
{PROMPT_INSTRUCTIONS}
|
|
72
74
|
""",
|
|
73
75
|
dependencies=[
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
"pydantic",
|
|
77
|
+
"boto3",
|
|
76
78
|
],
|
|
77
79
|
)
|
|
78
80
|
|
|
79
81
|
|
|
80
|
-
@mcp.tool(name=
|
|
82
|
+
@mcp.tool(name="generate_image")
|
|
81
83
|
async def mcp_generate_image(
|
|
82
84
|
ctx: Context,
|
|
83
85
|
prompt: str = Field(
|
|
84
|
-
description=
|
|
86
|
+
description="The text description of the image to generate (1-1024 characters)"
|
|
85
87
|
),
|
|
86
88
|
negative_prompt: Optional[str] = Field(
|
|
87
89
|
default=None,
|
|
88
|
-
description=
|
|
90
|
+
description="Text to define what not to include in the image (1-1024 characters)",
|
|
89
91
|
),
|
|
90
92
|
filename: Optional[str] = Field(
|
|
91
|
-
default=None,
|
|
93
|
+
default=None,
|
|
94
|
+
description="The name of the file to save the image to (without extension)",
|
|
92
95
|
),
|
|
93
96
|
width: int = Field(
|
|
94
97
|
default=DEFAULT_WIDTH,
|
|
95
|
-
description=
|
|
98
|
+
description="The width of the generated image (320-4096, divisible by 16)",
|
|
96
99
|
),
|
|
97
100
|
height: int = Field(
|
|
98
101
|
default=DEFAULT_HEIGHT,
|
|
99
|
-
description=
|
|
102
|
+
description="The height of the generated image (320-4096, divisible by 16)",
|
|
100
103
|
),
|
|
101
104
|
quality: str = Field(
|
|
102
105
|
default=DEFAULT_QUALITY,
|
|
@@ -104,11 +107,14 @@ async def mcp_generate_image(
|
|
|
104
107
|
),
|
|
105
108
|
cfg_scale: float = Field(
|
|
106
109
|
default=DEFAULT_CFG_SCALE,
|
|
107
|
-
description=
|
|
110
|
+
description="How strongly the image adheres to the prompt (1.1-10.0)",
|
|
111
|
+
),
|
|
112
|
+
seed: Optional[int] = Field(
|
|
113
|
+
default=None, description="Seed for generation (0-858,993,459)"
|
|
108
114
|
),
|
|
109
|
-
seed: Optional[int] = Field(default=None, description='Seed for generation (0-858,993,459)'),
|
|
110
115
|
number_of_images: int = Field(
|
|
111
|
-
default=DEFAULT_NUMBER_OF_IMAGES,
|
|
116
|
+
default=DEFAULT_NUMBER_OF_IMAGES,
|
|
117
|
+
description="The number of images to generate (1-5)",
|
|
112
118
|
),
|
|
113
119
|
workspace_dir: Optional[str] = Field(
|
|
114
120
|
default=None,
|
|
@@ -155,7 +161,7 @@ async def mcp_generate_image(
|
|
|
155
161
|
|
|
156
162
|
try:
|
|
157
163
|
logger.info(
|
|
158
|
-
f
|
|
164
|
+
f"Generating image with text prompt, quality: {quality}, cfg_scale: {cfg_scale}"
|
|
159
165
|
)
|
|
160
166
|
response = await generate_image_with_text(
|
|
161
167
|
prompt=prompt,
|
|
@@ -171,54 +177,62 @@ async def mcp_generate_image(
|
|
|
171
177
|
workspace_dir=workspace_dir,
|
|
172
178
|
)
|
|
173
179
|
|
|
174
|
-
if response.status ==
|
|
180
|
+
if response.status == "success":
|
|
175
181
|
# return response.paths
|
|
176
182
|
return McpImageGenerationResponse(
|
|
177
|
-
status=
|
|
178
|
-
paths=[f
|
|
183
|
+
status="success",
|
|
184
|
+
paths=[f"file://{path}" for path in response.paths],
|
|
179
185
|
)
|
|
180
186
|
else:
|
|
181
|
-
logger.error(f
|
|
182
|
-
await ctx.error(f
|
|
187
|
+
logger.error(f"Image generation returned error status: {response.message}")
|
|
188
|
+
await ctx.error(f"Failed to generate image: {response.message}") # type: ignore
|
|
183
189
|
# Return empty image or raise exception based on requirements
|
|
184
|
-
raise Exception(f
|
|
190
|
+
raise Exception(f"Failed to generate image: {response.message}")
|
|
185
191
|
except Exception as e:
|
|
186
|
-
logger.error(f
|
|
187
|
-
await ctx.error(f
|
|
192
|
+
logger.error(f"Error in mcp_generate_image: {str(e)}")
|
|
193
|
+
await ctx.error(f"Error generating image: {str(e)}") # type: ignore
|
|
188
194
|
raise
|
|
189
195
|
|
|
190
196
|
|
|
191
|
-
@mcp.tool(name=
|
|
197
|
+
@mcp.tool(name="generate_image_with_colors")
|
|
192
198
|
async def mcp_generate_image_with_colors(
|
|
193
199
|
ctx: Context,
|
|
194
200
|
prompt: str = Field(
|
|
195
|
-
description=
|
|
201
|
+
description="The text description of the image to generate (1-1024 characters)"
|
|
196
202
|
),
|
|
197
203
|
colors: List[str] = Field(
|
|
198
204
|
description='List of up to 10 hexadecimal color values (e.g., "#FF9800")'
|
|
199
205
|
),
|
|
200
206
|
negative_prompt: Optional[str] = Field(
|
|
201
207
|
default=None,
|
|
202
|
-
description=
|
|
208
|
+
description="Text to define what not to include in the image (1-1024 characters)",
|
|
203
209
|
),
|
|
204
210
|
filename: Optional[str] = Field(
|
|
205
|
-
default=None,
|
|
211
|
+
default=None,
|
|
212
|
+
description="The name of the file to save the image to (without extension)",
|
|
206
213
|
),
|
|
207
214
|
width: int = Field(
|
|
208
|
-
default=1024,
|
|
215
|
+
default=1024,
|
|
216
|
+
description="The width of the generated image (320-4096, divisible by 16)",
|
|
209
217
|
),
|
|
210
218
|
height: int = Field(
|
|
211
|
-
default=1024,
|
|
219
|
+
default=1024,
|
|
220
|
+
description="The height of the generated image (320-4096, divisible by 16)",
|
|
212
221
|
),
|
|
213
222
|
quality: str = Field(
|
|
214
|
-
default=
|
|
223
|
+
default="standard",
|
|
215
224
|
description='The quality of the generated image ("standard" or "premium")',
|
|
216
225
|
),
|
|
217
226
|
cfg_scale: float = Field(
|
|
218
|
-
default=6.5,
|
|
227
|
+
default=6.5,
|
|
228
|
+
description="How strongly the image adheres to the prompt (1.1-10.0)",
|
|
229
|
+
),
|
|
230
|
+
seed: Optional[int] = Field(
|
|
231
|
+
default=None, description="Seed for generation (0-858,993,459)"
|
|
232
|
+
),
|
|
233
|
+
number_of_images: int = Field(
|
|
234
|
+
default=1, description="The number of images to generate (1-5)"
|
|
219
235
|
),
|
|
220
|
-
seed: Optional[int] = Field(default=None, description='Seed for generation (0-858,993,459)'),
|
|
221
|
-
number_of_images: int = Field(default=1, description='The number of images to generate (1-5)'),
|
|
222
236
|
workspace_dir: Optional[str] = Field(
|
|
223
237
|
default=None,
|
|
224
238
|
description="The current workspace directory where the image should be saved. CRITICAL: Assistant must always provide this parameter to save images to the user's current project.",
|
|
@@ -260,9 +274,9 @@ async def mcp_generate_image_with_colors(
|
|
|
260
274
|
)
|
|
261
275
|
|
|
262
276
|
try:
|
|
263
|
-
color_hex_list =
|
|
277
|
+
color_hex_list = ", ".join(colors[:3]) + (", ..." if len(colors) > 3 else "")
|
|
264
278
|
logger.info(
|
|
265
|
-
f
|
|
279
|
+
f"Generating color-guided image with colors: [{color_hex_list}], quality: {quality}"
|
|
266
280
|
)
|
|
267
281
|
|
|
268
282
|
response = await generate_image_with_colors(
|
|
@@ -280,45 +294,51 @@ async def mcp_generate_image_with_colors(
|
|
|
280
294
|
workspace_dir=workspace_dir,
|
|
281
295
|
)
|
|
282
296
|
|
|
283
|
-
if response.status ==
|
|
297
|
+
if response.status == "success":
|
|
284
298
|
return McpImageGenerationResponse(
|
|
285
|
-
status=
|
|
286
|
-
paths=[f
|
|
299
|
+
status="success",
|
|
300
|
+
paths=[f"file://{path}" for path in response.paths],
|
|
287
301
|
)
|
|
288
302
|
else:
|
|
289
303
|
logger.error(
|
|
290
|
-
f
|
|
304
|
+
f"Color-guided image generation returned error status: {response.message}"
|
|
305
|
+
)
|
|
306
|
+
await ctx.error(
|
|
307
|
+
f"Failed to generate color-guided image: {response.message}"
|
|
308
|
+
)
|
|
309
|
+
raise Exception(
|
|
310
|
+
f"Failed to generate color-guided image: {response.message}"
|
|
291
311
|
)
|
|
292
|
-
await ctx.error(f'Failed to generate color-guided image: {response.message}')
|
|
293
|
-
raise Exception(f'Failed to generate color-guided image: {response.message}')
|
|
294
312
|
except Exception as e:
|
|
295
|
-
logger.error(f
|
|
296
|
-
await ctx.error(f
|
|
313
|
+
logger.error(f"Error in mcp_generate_image_with_colors: {str(e)}")
|
|
314
|
+
await ctx.error(f"Error generating color-guided image: {str(e)}")
|
|
297
315
|
raise
|
|
298
316
|
|
|
299
317
|
|
|
300
318
|
def main():
|
|
301
319
|
"""Run the MCP server with CLI argument support."""
|
|
302
|
-
logger.info(
|
|
320
|
+
logger.info("Starting nova-canvas-mcp-server MCP server")
|
|
303
321
|
|
|
304
322
|
parser = argparse.ArgumentParser(
|
|
305
|
-
description=
|
|
323
|
+
description="MCP server for generating images using Amazon Nova Canvas"
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument("--sse", action="store_true", help="Use SSE transport")
|
|
326
|
+
parser.add_argument(
|
|
327
|
+
"--port", type=int, default=8888, help="Port to run the server on"
|
|
306
328
|
)
|
|
307
|
-
parser.add_argument('--sse', action='store_true', help='Use SSE transport')
|
|
308
|
-
parser.add_argument('--port', type=int, default=8888, help='Port to run the server on')
|
|
309
329
|
|
|
310
330
|
args = parser.parse_args()
|
|
311
|
-
logger.debug(f
|
|
331
|
+
logger.debug(f"Parsed arguments: sse={args.sse}, port={args.port}")
|
|
312
332
|
|
|
313
333
|
# Run server with appropriate transport
|
|
314
334
|
if args.sse:
|
|
315
|
-
logger.info(f
|
|
335
|
+
logger.info(f"Using SSE transport on port {args.port}")
|
|
316
336
|
mcp.settings.port = args.port
|
|
317
|
-
mcp.run(transport=
|
|
337
|
+
mcp.run(transport="sse")
|
|
318
338
|
else:
|
|
319
|
-
logger.info(
|
|
339
|
+
logger.info("Using standard stdio transport")
|
|
320
340
|
mcp.run()
|
|
321
341
|
|
|
322
342
|
|
|
323
|
-
if __name__ ==
|
|
343
|
+
if __name__ == "__main__":
|
|
324
344
|
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.nova-canvas-mcp-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10652
|
|
4
4
|
Summary: An AWS Labs Model Context Protocol (MCP) server for Amazon Nova Canvas
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Requires-Dist: boto3>=1.37.24
|
|
@@ -9,7 +9,7 @@ Requires-Dist: mcp[cli]>=1.6.0
|
|
|
9
9
|
Requires-Dist: pydantic>=2.11.1
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Nova Canvas MCP Server
|
|
13
13
|
|
|
14
14
|
MCP server for generating images using Amazon Nova Canvas
|
|
15
15
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
awslabs/__init__.py,sha256=4zfFn3N0BkvQmMTAIvV_QAbKp6GWzrwaUN17YeRoChM,115
|
|
2
|
+
awslabs/nova_canvas_mcp_server/__init__.py,sha256=W7bzpkX1o3FURvzhQujITA3csYdIPA41UrCBnl99m8A,60
|
|
3
|
+
awslabs/nova_canvas_mcp_server/consts.py,sha256=jWKxZvmHi_5l2IULP5mHJLI3FLTx_4Y_KmC390ySWvA,2590
|
|
4
|
+
awslabs/nova_canvas_mcp_server/models.py,sha256=VxqfLSAfe3MiNWDP61tKu3UUOzgwqE2ko0U5n4Fxz-E,9963
|
|
5
|
+
awslabs/nova_canvas_mcp_server/novacanvas.py,sha256=pLnEB4qmQ3_IO2VgatmTi164g1NEhS11UkTL0uVjiEk,15305
|
|
6
|
+
awslabs/nova_canvas_mcp_server/server.py,sha256=mNN8MHLnpz9ZrGYZFTismDM0pRWsoQ9lknTmaPapgN0,12464
|
|
7
|
+
awslabs_nova_canvas_mcp_server-0.1.10652.dist-info/METADATA,sha256=EtSFRGpmUQrtJgGMmTNdyNRNrVIUbh7VTHPXyytglbI,2981
|
|
8
|
+
awslabs_nova_canvas_mcp_server-0.1.10652.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
awslabs_nova_canvas_mcp_server-0.1.10652.dist-info/entry_points.txt,sha256=v8V4vn8YuugOSL7w_sUxz-M0EDZNZU2_ydJZDd31pGI,94
|
|
10
|
+
awslabs_nova_canvas_mcp_server-0.1.10652.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
awslabs/__init__.py,sha256=4zfFn3N0BkvQmMTAIvV_QAbKp6GWzrwaUN17YeRoChM,115
|
|
2
|
-
awslabs/nova_canvas_mcp_server/__init__.py,sha256=D1JLDjoqRWgJm28RaKjBtIzAsuc31Ilg8r5-hv8I0ZU,60
|
|
3
|
-
awslabs/nova_canvas_mcp_server/consts.py,sha256=1qnIsWXKsg7R8JpWalgns0vPmBAHu6f9oI8hylhBuuo,2590
|
|
4
|
-
awslabs/nova_canvas_mcp_server/models.py,sha256=tYJeeTKhU_6OJxKJBvKyAzgC46-Dexx_o5up539jqi4,9935
|
|
5
|
-
awslabs/nova_canvas_mcp_server/novacanvas.py,sha256=ltHqnH5slEfq52jF69xE59pam1pEXmIRaGYnWFtfC04,15017
|
|
6
|
-
awslabs/nova_canvas_mcp_server/server.py,sha256=pZBwGwOU_3CoSfi9PHnfBmqI7g6cYqRGCTeF3Fg-sIk,12272
|
|
7
|
-
awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/METADATA,sha256=QsFHmvWwYAiuaPC3UNzBKI_6nJ0C0cjNT50KgDHmB-4,2989
|
|
8
|
-
awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
-
awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/entry_points.txt,sha256=v8V4vn8YuugOSL7w_sUxz-M0EDZNZU2_ydJZDd31pGI,94
|
|
10
|
-
awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/RECORD,,
|
|
File without changes
|