awslabs.nova-canvas-mcp-server 0.1.10233__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/__init__.py +2 -0
- awslabs/nova_canvas_mcp_server/__init__.py +3 -0
- awslabs/nova_canvas_mcp_server/consts.py +61 -0
- awslabs/nova_canvas_mcp_server/models.py +287 -0
- awslabs/nova_canvas_mcp_server/novacanvas.py +396 -0
- awslabs/nova_canvas_mcp_server/server.py +324 -0
- awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/METADATA +74 -0
- awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/RECORD +10 -0
- awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/WHEEL +4 -0
- awslabs_nova_canvas_mcp_server-0.1.10233.dist-info/entry_points.txt +2 -0
awslabs/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Constants
|
|
2
|
+
NOVA_CANVAS_MODEL_ID = 'amazon.nova-canvas-v1:0'
|
|
3
|
+
DEFAULT_WIDTH = 1024
|
|
4
|
+
DEFAULT_HEIGHT = 1024
|
|
5
|
+
DEFAULT_QUALITY = 'standard'
|
|
6
|
+
DEFAULT_CFG_SCALE = 6.5
|
|
7
|
+
DEFAULT_NUMBER_OF_IMAGES = 1
|
|
8
|
+
DEFAULT_OUTPUT_DIR = 'output' # Default directory inside workspace_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Nova Canvas Prompt Best Practices
|
|
12
|
+
PROMPT_INSTRUCTIONS = """
|
|
13
|
+
# Amazon Nova Canvas Prompting Best Practices
|
|
14
|
+
|
|
15
|
+
## General Guidelines
|
|
16
|
+
|
|
17
|
+
- Prompts must be no longer than 1024 characters. For very long prompts, place the least important details near the end.
|
|
18
|
+
- Do not use negation words like "no", "not", "without" in your prompt. The model doesn't understand negation and will result in the opposite of what you intend.
|
|
19
|
+
- Use negative prompts (via the `negative_prompt` parameter) to specify objects or characteristics to exclude from the image.
|
|
20
|
+
- Omit negation words from your negative prompts as well.
|
|
21
|
+
|
|
22
|
+
## Effective Prompt Structure
|
|
23
|
+
|
|
24
|
+
An effective prompt often includes short descriptions of:
|
|
25
|
+
|
|
26
|
+
1. The subject
|
|
27
|
+
2. The environment
|
|
28
|
+
3. (optional) The position or pose of the subject
|
|
29
|
+
4. (optional) Lighting description
|
|
30
|
+
5. (optional) Camera position/framing
|
|
31
|
+
6. (optional) The visual style or medium ("photo", "illustration", "painting", etc.)
|
|
32
|
+
|
|
33
|
+
## Refining Results
|
|
34
|
+
|
|
35
|
+
When the output is close to what you want but not perfect:
|
|
36
|
+
|
|
37
|
+
1. Use a consistent `seed` value and make small changes to your prompt or negative prompt.
|
|
38
|
+
2. Once the prompt is refined, generate more variations using the same prompt but different `seed` values.
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Example 1: Stock Photo
|
|
43
|
+
**Prompt:** "realistic editorial photo of female teacher standing at a blackboard with a warm smile"
|
|
44
|
+
**Negative Prompt:** "crossed arms"
|
|
45
|
+
|
|
46
|
+
### Example 2: Story Illustration
|
|
47
|
+
**Prompt:** "whimsical and ethereal soft-shaded story illustration: A woman in a large hat stands at the ship's railing looking out across the ocean"
|
|
48
|
+
**Negative Prompt:** "clouds, waves"
|
|
49
|
+
|
|
50
|
+
### Example 3: Pre-visualization for TV/Film
|
|
51
|
+
**Prompt:** "drone view of a dark river winding through a stark Iceland landscape, cinematic quality"
|
|
52
|
+
|
|
53
|
+
### Example 4: Fashion/Editorial Content
|
|
54
|
+
**Prompt:** "A cool looking stylish man in an orange jacket, dark skin, wearing reflective glasses. Shot from slightly low angle, face and chest in view, aqua blue sleek building shapes in background."
|
|
55
|
+
|
|
56
|
+
## Using Negative Prompts
|
|
57
|
+
|
|
58
|
+
Negative prompts can be surprisingly useful. Use them to exclude objects or style characteristics that might otherwise naturally occur as a result of your main prompt.
|
|
59
|
+
|
|
60
|
+
For example, adding "waves, clouds" as a negative prompt to a ship scene will result in a cleaner, more minimal composition.
|
|
61
|
+
"""
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Pydantic models for Amazon Nova Canvas image generation."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Quality(str, Enum):
|
|
11
|
+
"""Quality options for image generation.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
STANDARD: Standard quality image generation.
|
|
15
|
+
PREMIUM: Premium quality image generation with enhanced details.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
STANDARD = 'standard'
|
|
19
|
+
PREMIUM = 'premium'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskType(str, Enum):
|
|
23
|
+
"""Task types for image generation.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
TEXT_IMAGE: Generate an image from text description.
|
|
27
|
+
COLOR_GUIDED_GENERATION: Generate an image guided by both text and color palette.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
TEXT_IMAGE = 'TEXT_IMAGE'
|
|
31
|
+
COLOR_GUIDED_GENERATION = 'COLOR_GUIDED_GENERATION'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageGenerationConfig(BaseModel):
|
|
35
|
+
"""Configuration for image generation.
|
|
36
|
+
|
|
37
|
+
This model defines the parameters that control the image generation process,
|
|
38
|
+
including dimensions, quality, and generation settings.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
width: Width of the generated image (320-4096, must be divisible by 16).
|
|
42
|
+
height: Height of the generated image (320-4096, must be divisible by 16).
|
|
43
|
+
quality: Quality level of the generated image (standard or premium).
|
|
44
|
+
cfgScale: How strongly the image adheres to the prompt (1.1-10.0).
|
|
45
|
+
seed: Seed for reproducible generation (0-858993459).
|
|
46
|
+
numberOfImages: Number of images to generate (1-5).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
width: int = Field(default=1024, ge=320, le=4096)
|
|
50
|
+
height: int = Field(default=1024, ge=320, le=4096)
|
|
51
|
+
quality: Quality = Quality.STANDARD
|
|
52
|
+
cfgScale: float = Field(default=6.5, ge=1.1, le=10.0)
|
|
53
|
+
seed: int = Field(default_factory=lambda: random.randint(0, 858993459), ge=0, le=858993459)
|
|
54
|
+
numberOfImages: int = Field(default=1, ge=1, le=5)
|
|
55
|
+
|
|
56
|
+
@field_validator('width', 'height')
|
|
57
|
+
@classmethod
|
|
58
|
+
def must_be_divisible_by_16(cls, v: int) -> int:
|
|
59
|
+
"""Validate that width and height are divisible by 16.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
v: The width or height value to validate.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The validated value if it passes.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If the value is not divisible by 16.
|
|
69
|
+
"""
|
|
70
|
+
if v % 16 != 0:
|
|
71
|
+
raise ValueError('Value must be divisible by 16')
|
|
72
|
+
return v
|
|
73
|
+
|
|
74
|
+
@model_validator(mode='after')
|
|
75
|
+
def validate_aspect_ratio_and_total_pixels(self):
|
|
76
|
+
"""Validate aspect ratio and total pixel count.
|
|
77
|
+
|
|
78
|
+
Ensures that:
|
|
79
|
+
1. The aspect ratio is between 1:4 and 4:1
|
|
80
|
+
2. The total pixel count is less than 4,194,304
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The validated model if it passes.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If the aspect ratio or total pixel count is invalid.
|
|
87
|
+
"""
|
|
88
|
+
width = self.width
|
|
89
|
+
height = self.height
|
|
90
|
+
|
|
91
|
+
# Check aspect ratio between 1:4 and 4:1
|
|
92
|
+
aspect_ratio = width / height
|
|
93
|
+
if aspect_ratio < 0.25 or aspect_ratio > 4.0:
|
|
94
|
+
raise ValueError('Aspect ratio must be between 1:4 and 4:1')
|
|
95
|
+
|
|
96
|
+
# Check total pixel count
|
|
97
|
+
total_pixels = width * height
|
|
98
|
+
if total_pixels >= 4194304:
|
|
99
|
+
raise ValueError('Total pixel count must be less than 4,194,304')
|
|
100
|
+
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TextToImageParams(BaseModel):
|
|
105
|
+
"""Parameters for text-to-image generation.
|
|
106
|
+
|
|
107
|
+
This model defines the text prompts used to generate images.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
text: The text description of the image to generate (1-1024 characters).
|
|
111
|
+
negativeText: Optional text to define what not to include in the image (1-1024 characters).
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
text: str = Field(..., min_length=1, max_length=1024)
|
|
115
|
+
negativeText: Optional[str] = Field(default=None, min_length=1, max_length=1024)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ColorGuidedGenerationParams(BaseModel):
|
|
119
|
+
"""Parameters for color-guided generation.
|
|
120
|
+
|
|
121
|
+
This model defines the text prompts and color palette used to generate images.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
colors: List of hexadecimal color values (e.g., "#FF9800") to guide the image generation.
|
|
125
|
+
text: The text description of the image to generate (1-1024 characters).
|
|
126
|
+
negativeText: Optional text to define what not to include in the image (1-1024 characters).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
colors: List[str] = Field(..., max_length=10)
|
|
130
|
+
text: str = Field(..., min_length=1, max_length=1024)
|
|
131
|
+
negativeText: Optional[str] = Field(default=None, min_length=1, max_length=1024)
|
|
132
|
+
|
|
133
|
+
@field_validator('colors')
|
|
134
|
+
@classmethod
|
|
135
|
+
def validate_hex_colors(cls, v: List[str]) -> List[str]:
|
|
136
|
+
"""Validate that colors are in the correct hexadecimal format.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
v: List of color strings to validate.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The validated list if all colors pass.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If any color is not a valid hexadecimal color in the format '#RRGGBB'.
|
|
146
|
+
"""
|
|
147
|
+
hex_pattern = re.compile(r'^#[0-9A-Fa-f]{6}$')
|
|
148
|
+
for color in v:
|
|
149
|
+
if not hex_pattern.match(color):
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Color '{color}' is not a valid hexadecimal color in the format '#RRGGBB'"
|
|
152
|
+
)
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TextImageRequest(BaseModel):
|
|
157
|
+
"""Request model for text-to-image generation.
|
|
158
|
+
|
|
159
|
+
This model combines the task type, text parameters, and generation configuration
|
|
160
|
+
for a complete text-to-image request.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
taskType: The type of task (TEXT_IMAGE).
|
|
164
|
+
textToImageParams: Parameters for text-to-image generation.
|
|
165
|
+
imageGenerationConfig: Configuration for image generation.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
taskType: Literal[TaskType.TEXT_IMAGE] = TaskType.TEXT_IMAGE
|
|
169
|
+
textToImageParams: TextToImageParams
|
|
170
|
+
imageGenerationConfig: Optional[ImageGenerationConfig] = Field(
|
|
171
|
+
default_factory=ImageGenerationConfig
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# instead of overriding model_dump, we add a post-model_dump extension method
|
|
175
|
+
def to_api_dict(self) -> Dict[str, Any]:
|
|
176
|
+
"""Convert model to dictionary suitable for API requests.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A dictionary representation of the model suitable for API requests.
|
|
180
|
+
"""
|
|
181
|
+
text_to_image_params = self.textToImageParams.model_dump()
|
|
182
|
+
# Remove negativeText if it's None
|
|
183
|
+
if text_to_image_params.get('negativeText') is None:
|
|
184
|
+
text_to_image_params.pop('negativeText', None)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
'taskType': self.taskType,
|
|
188
|
+
'textToImageParams': text_to_image_params,
|
|
189
|
+
'imageGenerationConfig': self.imageGenerationConfig.model_dump()
|
|
190
|
+
if self.imageGenerationConfig
|
|
191
|
+
else None,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ColorGuidedRequest(BaseModel):
|
|
196
|
+
"""Request model for color-guided generation.
|
|
197
|
+
|
|
198
|
+
This model combines the task type, color-guided parameters, and generation configuration
|
|
199
|
+
for a complete color-guided generation request.
|
|
200
|
+
|
|
201
|
+
Attributes:
|
|
202
|
+
taskType: The type of task (COLOR_GUIDED_GENERATION).
|
|
203
|
+
colorGuidedGenerationParams: Parameters for color-guided generation.
|
|
204
|
+
imageGenerationConfig: Configuration for image generation.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
taskType: Literal[TaskType.COLOR_GUIDED_GENERATION] = TaskType.COLOR_GUIDED_GENERATION
|
|
208
|
+
colorGuidedGenerationParams: ColorGuidedGenerationParams
|
|
209
|
+
imageGenerationConfig: Optional[ImageGenerationConfig] = Field(
|
|
210
|
+
default_factory=ImageGenerationConfig
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# instead of overriding model_dump, we add a post-model_dump extension method
|
|
214
|
+
def to_api_dict(self) -> Dict[str, Any]:
|
|
215
|
+
"""Convert model to dictionary suitable for API requests.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
A dictionary representation of the model suitable for API requests.
|
|
219
|
+
"""
|
|
220
|
+
color_guided_params = self.colorGuidedGenerationParams.model_dump()
|
|
221
|
+
# Remove negativeText if it's None
|
|
222
|
+
if color_guided_params.get('negativeText') is None:
|
|
223
|
+
color_guided_params.pop('negativeText', None)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
'taskType': self.taskType,
|
|
227
|
+
'colorGuidedGenerationParams': color_guided_params,
|
|
228
|
+
'imageGenerationConfig': self.imageGenerationConfig.model_dump()
|
|
229
|
+
if self.imageGenerationConfig
|
|
230
|
+
else None,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class McpImageGenerationResponse(BaseModel):
|
|
235
|
+
"""Response from image generation API.
|
|
236
|
+
|
|
237
|
+
This model represents the response from the Amazon Nova Canvas API
|
|
238
|
+
for both text-to-image and color-guided image generation.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
status: str
|
|
242
|
+
paths: List[str]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ImageGenerationResponse(BaseModel):
|
|
246
|
+
"""Response from image generation API.
|
|
247
|
+
|
|
248
|
+
This model represents the response from the Amazon Nova Canvas API
|
|
249
|
+
for both text-to-image and color-guided image generation.
|
|
250
|
+
|
|
251
|
+
Attributes:
|
|
252
|
+
status: Status of the image generation request ('success' or 'error').
|
|
253
|
+
message: Message describing the result or error.
|
|
254
|
+
paths: List of paths to the generated image files.
|
|
255
|
+
images: List of PIL Image objects.
|
|
256
|
+
prompt: The text prompt used to generate the images.
|
|
257
|
+
negative_prompt: The negative prompt used to generate the images, if any.
|
|
258
|
+
colors: The colors used to guide the image generation, if any.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
status: str
|
|
262
|
+
message: str
|
|
263
|
+
paths: List[str]
|
|
264
|
+
prompt: str
|
|
265
|
+
negative_prompt: Optional[str] = None
|
|
266
|
+
colors: Optional[List[str]] = None
|
|
267
|
+
|
|
268
|
+
class Config:
|
|
269
|
+
"""Pydantic configuration."""
|
|
270
|
+
|
|
271
|
+
arbitrary_types_allowed = True # Allow PIL.Image.Image type
|
|
272
|
+
|
|
273
|
+
def __getitem__(self, key: str) -> Any:
|
|
274
|
+
"""Support dictionary-style access for backward compatibility.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
key: The attribute name to access.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The value of the attribute.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
KeyError: If the attribute does not exist.
|
|
284
|
+
"""
|
|
285
|
+
if hasattr(self, key):
|
|
286
|
+
return getattr(self, key)
|
|
287
|
+
raise KeyError(f"'{key}' not found in ImageGenerationResponse")
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Amazon Nova Canvas API interaction module.
|
|
2
|
+
|
|
3
|
+
This module provides functions for generating images using Amazon Nova Canvas
|
|
4
|
+
through the AWS Bedrock service. It handles the API requests, response processing,
|
|
5
|
+
and image saving functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
from .models import (
|
|
13
|
+
ColorGuidedGenerationParams,
|
|
14
|
+
ColorGuidedRequest,
|
|
15
|
+
ImageGenerationConfig,
|
|
16
|
+
ImageGenerationResponse,
|
|
17
|
+
Quality,
|
|
18
|
+
TextImageRequest,
|
|
19
|
+
TextToImageParams,
|
|
20
|
+
)
|
|
21
|
+
from awslabs.nova_canvas_mcp_server.consts import (
|
|
22
|
+
DEFAULT_CFG_SCALE,
|
|
23
|
+
DEFAULT_HEIGHT,
|
|
24
|
+
DEFAULT_NUMBER_OF_IMAGES,
|
|
25
|
+
DEFAULT_OUTPUT_DIR,
|
|
26
|
+
DEFAULT_QUALITY,
|
|
27
|
+
DEFAULT_WIDTH,
|
|
28
|
+
NOVA_CANVAS_MODEL_ID,
|
|
29
|
+
)
|
|
30
|
+
from loguru import logger
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from mypy_boto3_bedrock_runtime import BedrockRuntimeClient
|
|
36
|
+
else:
|
|
37
|
+
BedrockRuntimeClient = object
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_generated_images(
|
|
41
|
+
base64_images: List[str],
|
|
42
|
+
filename: Optional[str] = None,
|
|
43
|
+
number_of_images: int = DEFAULT_NUMBER_OF_IMAGES,
|
|
44
|
+
prefix: str = 'nova_canvas',
|
|
45
|
+
workspace_dir: Optional[str] = None,
|
|
46
|
+
) -> Dict[str, List]:
|
|
47
|
+
"""Save base64-encoded images to files.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base64_images: List of base64-encoded image data.
|
|
51
|
+
filename: Base filename to use (without extension). If None, a random name is generated.
|
|
52
|
+
number_of_images: Number of images being saved.
|
|
53
|
+
prefix: Prefix to use for randomly generated filenames.
|
|
54
|
+
workspace_dir: Directory where the images should be saved. If None, uses current directory.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary with lists of paths to the saved image files and PIL Image objects.
|
|
58
|
+
"""
|
|
59
|
+
logger.debug(f'Saving {len(base64_images)} images')
|
|
60
|
+
# Determine the output directory
|
|
61
|
+
if workspace_dir:
|
|
62
|
+
output_dir = os.path.join(workspace_dir, DEFAULT_OUTPUT_DIR)
|
|
63
|
+
else:
|
|
64
|
+
output_dir = DEFAULT_OUTPUT_DIR
|
|
65
|
+
|
|
66
|
+
# Create output directory if it doesn't exist
|
|
67
|
+
if not os.path.exists(output_dir):
|
|
68
|
+
os.makedirs(output_dir)
|
|
69
|
+
|
|
70
|
+
# Save the generated images
|
|
71
|
+
saved_paths: List[str] = []
|
|
72
|
+
for i, base64_image_data in enumerate(base64_images):
|
|
73
|
+
# Generate filename if not provided
|
|
74
|
+
if filename:
|
|
75
|
+
image_filename = (
|
|
76
|
+
f'{filename}_{i + 1}.png' if number_of_images > 1 else f'{filename}.png'
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
# Generate a random filename
|
|
80
|
+
random_id = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))
|
|
81
|
+
image_filename = f'{prefix}_{random_id}_{i + 1}.png'
|
|
82
|
+
|
|
83
|
+
# Decode the base64 image data
|
|
84
|
+
image_data = base64.b64decode(base64_image_data)
|
|
85
|
+
|
|
86
|
+
# Save the image
|
|
87
|
+
image_path = os.path.join(output_dir, image_filename)
|
|
88
|
+
with open(image_path, 'wb') as file:
|
|
89
|
+
file.write(image_data)
|
|
90
|
+
# Convert to absolute path
|
|
91
|
+
abs_image_path = os.path.abspath(image_path)
|
|
92
|
+
saved_paths.append(abs_image_path)
|
|
93
|
+
|
|
94
|
+
return {'paths': saved_paths}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def invoke_nova_canvas(
|
|
98
|
+
request_model_dict: Dict[str, Any],
|
|
99
|
+
bedrock_runtime_client: BedrockRuntimeClient,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
"""Invoke the Nova Canvas API with the given request.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
request_model_dict: Dictionary representation of the request model.
|
|
105
|
+
bedrock_runtime_client: BedrockRuntimeClient object.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary containing the API response.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
Exception: If the API call fails.
|
|
112
|
+
"""
|
|
113
|
+
logger.debug('Invoking Nova Canvas API')
|
|
114
|
+
|
|
115
|
+
# Convert the request payload to JSON
|
|
116
|
+
request = json.dumps(request_model_dict)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Invoke the model
|
|
120
|
+
logger.info(f'Sending request to Nova Canvas model: {NOVA_CANVAS_MODEL_ID}')
|
|
121
|
+
response = bedrock_runtime_client.invoke_model(modelId=NOVA_CANVAS_MODEL_ID, body=request)
|
|
122
|
+
|
|
123
|
+
# Decode the response body
|
|
124
|
+
result = json.loads(response['body'].read())
|
|
125
|
+
logger.info('Nova Canvas API call successful')
|
|
126
|
+
return result
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f'Nova Canvas API call failed: {str(e)}')
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def generate_image_with_text(
|
|
133
|
+
prompt: str,
|
|
134
|
+
bedrock_runtime_client: BedrockRuntimeClient,
|
|
135
|
+
negative_prompt: Optional[str] = None,
|
|
136
|
+
filename: Optional[str] = None,
|
|
137
|
+
width: int = DEFAULT_WIDTH,
|
|
138
|
+
height: int = DEFAULT_HEIGHT,
|
|
139
|
+
quality: str = DEFAULT_QUALITY,
|
|
140
|
+
cfg_scale: float = DEFAULT_CFG_SCALE,
|
|
141
|
+
seed: Optional[int] = None,
|
|
142
|
+
number_of_images: int = DEFAULT_NUMBER_OF_IMAGES,
|
|
143
|
+
workspace_dir: Optional[str] = None,
|
|
144
|
+
) -> ImageGenerationResponse:
|
|
145
|
+
"""Generate an image using Amazon Nova Canvas with text prompt.
|
|
146
|
+
|
|
147
|
+
This function uses Amazon Nova Canvas to generate images based on a text prompt.
|
|
148
|
+
The generated image will be saved to a file and the path will be returned.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
prompt: The text description of the image to generate (1-1024 characters).
|
|
152
|
+
bedrock_runtime_client: BedrockRuntimeClient object.
|
|
153
|
+
negative_prompt: Text to define what not to include in the image (1-1024 characters).
|
|
154
|
+
filename: The name of the file to save the image to (without extension).
|
|
155
|
+
If not provided, a random name will be generated.
|
|
156
|
+
width: The width of the generated image (320-4096, divisible by 16).
|
|
157
|
+
height: The height of the generated image (320-4096, divisible by 16).
|
|
158
|
+
quality: The quality of the generated image ("standard" or "premium").
|
|
159
|
+
cfg_scale: How strongly the image adheres to the prompt (1.1-10.0).
|
|
160
|
+
seed: Seed for generation (0-858,993,459). Random if not provided.
|
|
161
|
+
number_of_images: The number of images to generate (1-5).
|
|
162
|
+
workspace_dir: Directory where the images should be saved. If None, uses current directory.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ImageGenerationResponse: An object containing the paths to the generated images,
|
|
166
|
+
PIL Image objects, and status information.
|
|
167
|
+
"""
|
|
168
|
+
logger.debug(f"Generating text-to-image with prompt: '{prompt[:30]}...' ({width}x{height})")
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Validate input parameters using Pydantic
|
|
172
|
+
try:
|
|
173
|
+
logger.debug('Validating parameters and creating request model')
|
|
174
|
+
|
|
175
|
+
# Create image generation config
|
|
176
|
+
config = ImageGenerationConfig(
|
|
177
|
+
width=width,
|
|
178
|
+
height=height,
|
|
179
|
+
quality=Quality.STANDARD if quality == DEFAULT_QUALITY else Quality.PREMIUM,
|
|
180
|
+
cfgScale=cfg_scale,
|
|
181
|
+
seed=seed if seed is not None else random.randint(0, 858993459),
|
|
182
|
+
numberOfImages=number_of_images,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Create text-to-image params
|
|
186
|
+
# The Nova Canvas API doesn't accept null for negativeText
|
|
187
|
+
if negative_prompt is not None:
|
|
188
|
+
text_params = TextToImageParams(text=prompt, negativeText=negative_prompt)
|
|
189
|
+
else:
|
|
190
|
+
text_params = TextToImageParams(text=prompt)
|
|
191
|
+
|
|
192
|
+
# Create the full request
|
|
193
|
+
request_model = TextImageRequest(
|
|
194
|
+
textToImageParams=text_params, imageGenerationConfig=config
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Convert model to dictionary
|
|
198
|
+
request_model_dict = request_model.to_api_dict()
|
|
199
|
+
logger.info('Request validation successful')
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f'Parameter validation failed: {str(e)}')
|
|
203
|
+
return ImageGenerationResponse(
|
|
204
|
+
status='error',
|
|
205
|
+
message=f'Validation error: {str(e)}',
|
|
206
|
+
paths=[],
|
|
207
|
+
prompt=prompt,
|
|
208
|
+
negative_prompt=negative_prompt,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Invoke the Nova Canvas API
|
|
213
|
+
logger.debug('Sending request to Nova Canvas API')
|
|
214
|
+
model_response = await invoke_nova_canvas(request_model_dict, bedrock_runtime_client)
|
|
215
|
+
|
|
216
|
+
# Extract the image data
|
|
217
|
+
base64_images = model_response['images']
|
|
218
|
+
logger.info(f'Received {len(base64_images)} images from Nova Canvas API')
|
|
219
|
+
|
|
220
|
+
# Save the generated images
|
|
221
|
+
result = save_generated_images(
|
|
222
|
+
base64_images,
|
|
223
|
+
filename,
|
|
224
|
+
number_of_images,
|
|
225
|
+
prefix='nova_canvas',
|
|
226
|
+
workspace_dir=workspace_dir,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
logger.info(f'Successfully generated {len(result["paths"])} image(s)')
|
|
230
|
+
return ImageGenerationResponse(
|
|
231
|
+
status='success',
|
|
232
|
+
message=f'Generated {len(result["paths"])} image(s)',
|
|
233
|
+
paths=result['paths'],
|
|
234
|
+
prompt=prompt,
|
|
235
|
+
negative_prompt=negative_prompt,
|
|
236
|
+
)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f'Image generation failed: {str(e)}')
|
|
239
|
+
return ImageGenerationResponse(
|
|
240
|
+
status='error',
|
|
241
|
+
message=str(e),
|
|
242
|
+
paths=[],
|
|
243
|
+
prompt=prompt,
|
|
244
|
+
negative_prompt=negative_prompt,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f'Unexpected error in generate_image_with_text: {str(e)}')
|
|
249
|
+
return ImageGenerationResponse(
|
|
250
|
+
status='error',
|
|
251
|
+
message=str(e),
|
|
252
|
+
paths=[],
|
|
253
|
+
prompt=prompt,
|
|
254
|
+
negative_prompt=negative_prompt,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def generate_image_with_colors(
|
|
259
|
+
prompt: str,
|
|
260
|
+
colors: List[str],
|
|
261
|
+
bedrock_runtime_client: BedrockRuntimeClient,
|
|
262
|
+
negative_prompt: Optional[str] = None,
|
|
263
|
+
filename: Optional[str] = None,
|
|
264
|
+
width: int = DEFAULT_WIDTH,
|
|
265
|
+
height: int = DEFAULT_HEIGHT,
|
|
266
|
+
quality: str = DEFAULT_QUALITY,
|
|
267
|
+
cfg_scale: float = DEFAULT_CFG_SCALE,
|
|
268
|
+
seed: Optional[int] = None,
|
|
269
|
+
number_of_images: int = DEFAULT_NUMBER_OF_IMAGES,
|
|
270
|
+
workspace_dir: Optional[str] = None,
|
|
271
|
+
) -> ImageGenerationResponse:
|
|
272
|
+
"""Generate an image using Amazon Nova Canvas with color guidance.
|
|
273
|
+
|
|
274
|
+
This function uses Amazon Nova Canvas to generate images based on a text prompt and color palette.
|
|
275
|
+
The generated image will be saved to a file and the path will be returned.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
prompt: The text description of the image to generate (1-1024 characters).
|
|
279
|
+
colors: List of up to 10 hexadecimal color values (e.g., "#FF9800").
|
|
280
|
+
bedrock_runtime_client: BedrockRuntimeClient object.
|
|
281
|
+
negative_prompt: Text to define what not to include in the image (1-1024 characters).
|
|
282
|
+
filename: The name of the file to save the image to (without extension).
|
|
283
|
+
If not provided, a random name will be generated.
|
|
284
|
+
width: The width of the generated image (320-4096, divisible by 16).
|
|
285
|
+
height: The height of the generated image (320-4096, divisible by 16).
|
|
286
|
+
quality: The quality of the generated image ("standard" or "premium").
|
|
287
|
+
cfg_scale: How strongly the image adheres to the prompt (1.1-10.0).
|
|
288
|
+
seed: Seed for generation (0-858,993,459). Random if not provided.
|
|
289
|
+
number_of_images: The number of images to generate (1-5).
|
|
290
|
+
workspace_dir: Directory where the images should be saved. If None, uses current directory.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
ImageGenerationResponse: An object containing the paths to the generated images,
|
|
294
|
+
PIL Image objects, and status information.
|
|
295
|
+
"""
|
|
296
|
+
logger.debug(
|
|
297
|
+
f"Generating color-guided image with prompt: '{prompt[:30]}...' and {len(colors)} colors"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Validate input parameters using Pydantic
|
|
302
|
+
try:
|
|
303
|
+
logger.debug('Validating parameters and creating color-guided request model')
|
|
304
|
+
|
|
305
|
+
# Create image generation config
|
|
306
|
+
config = ImageGenerationConfig(
|
|
307
|
+
width=width,
|
|
308
|
+
height=height,
|
|
309
|
+
quality=Quality.STANDARD if quality == DEFAULT_QUALITY else Quality.PREMIUM,
|
|
310
|
+
cfgScale=cfg_scale,
|
|
311
|
+
seed=seed if seed is not None else random.randint(0, 858993459),
|
|
312
|
+
numberOfImages=number_of_images,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Create color-guided params
|
|
316
|
+
# The Nova Canvas API doesn't accept null for negativeText
|
|
317
|
+
if negative_prompt is not None:
|
|
318
|
+
color_params = ColorGuidedGenerationParams(
|
|
319
|
+
colors=colors,
|
|
320
|
+
text=prompt,
|
|
321
|
+
negativeText=negative_prompt,
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
color_params = ColorGuidedGenerationParams(
|
|
325
|
+
colors=colors,
|
|
326
|
+
text=prompt,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Create the full request
|
|
330
|
+
request_model = ColorGuidedRequest(
|
|
331
|
+
colorGuidedGenerationParams=color_params, imageGenerationConfig=config
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Convert model to dictionary
|
|
335
|
+
request_model_dict = request_model.to_api_dict()
|
|
336
|
+
logger.info('Color-guided request validation successful')
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f'Color-guided parameter validation failed: {str(e)}')
|
|
340
|
+
return ImageGenerationResponse(
|
|
341
|
+
status='error',
|
|
342
|
+
message=f'Validation error: {str(e)}',
|
|
343
|
+
paths=[],
|
|
344
|
+
prompt=prompt,
|
|
345
|
+
negative_prompt=negative_prompt,
|
|
346
|
+
colors=colors,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
# Invoke the Nova Canvas API
|
|
351
|
+
logger.debug('Sending color-guided request to Nova Canvas API')
|
|
352
|
+
model_response = await invoke_nova_canvas(request_model_dict, bedrock_runtime_client)
|
|
353
|
+
|
|
354
|
+
# Extract the image data
|
|
355
|
+
base64_images = model_response['images']
|
|
356
|
+
logger.info(f'Received {len(base64_images)} images from Nova Canvas API')
|
|
357
|
+
|
|
358
|
+
# Save the generated images
|
|
359
|
+
result = save_generated_images(
|
|
360
|
+
base64_images,
|
|
361
|
+
filename,
|
|
362
|
+
number_of_images,
|
|
363
|
+
prefix='nova_canvas_color',
|
|
364
|
+
workspace_dir=workspace_dir,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
logger.info(f'Successfully generated {len(result["paths"])} color-guided image(s)')
|
|
368
|
+
return ImageGenerationResponse(
|
|
369
|
+
status='success',
|
|
370
|
+
message=f'Generated {len(result["paths"])} image(s)',
|
|
371
|
+
paths=result['paths'],
|
|
372
|
+
prompt=prompt,
|
|
373
|
+
negative_prompt=negative_prompt,
|
|
374
|
+
colors=colors,
|
|
375
|
+
)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logger.error(f'Color-guided image generation failed: {str(e)}')
|
|
378
|
+
return ImageGenerationResponse(
|
|
379
|
+
status='error',
|
|
380
|
+
message=str(e),
|
|
381
|
+
paths=[],
|
|
382
|
+
prompt=prompt,
|
|
383
|
+
negative_prompt=negative_prompt,
|
|
384
|
+
colors=colors,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(f'Unexpected error in generate_image_with_colors: {str(e)}')
|
|
389
|
+
return ImageGenerationResponse(
|
|
390
|
+
status='error',
|
|
391
|
+
message=str(e),
|
|
392
|
+
paths=[],
|
|
393
|
+
prompt=prompt,
|
|
394
|
+
negative_prompt=negative_prompt,
|
|
395
|
+
colors=colors,
|
|
396
|
+
)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Nova Canvas MCP Server implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import boto3
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from awslabs.nova_canvas_mcp_server.consts import (
|
|
8
|
+
DEFAULT_CFG_SCALE,
|
|
9
|
+
DEFAULT_HEIGHT,
|
|
10
|
+
DEFAULT_NUMBER_OF_IMAGES,
|
|
11
|
+
DEFAULT_QUALITY,
|
|
12
|
+
DEFAULT_WIDTH,
|
|
13
|
+
PROMPT_INSTRUCTIONS,
|
|
14
|
+
)
|
|
15
|
+
from awslabs.nova_canvas_mcp_server.models import McpImageGenerationResponse
|
|
16
|
+
from awslabs.nova_canvas_mcp_server.novacanvas import (
|
|
17
|
+
generate_image_with_colors,
|
|
18
|
+
generate_image_with_text,
|
|
19
|
+
)
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from mcp.server.fastmcp import Context, FastMCP
|
|
22
|
+
from pydantic import Field
|
|
23
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Logging
|
|
27
|
+
logger.remove()
|
|
28
|
+
logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING'))
|
|
29
|
+
|
|
30
|
+
# Bedrock Runtime Client typing
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from mypy_boto3_bedrock_runtime import BedrockRuntimeClient
|
|
33
|
+
else:
|
|
34
|
+
BedrockRuntimeClient = object
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Bedrock Runtime Client
|
|
38
|
+
bedrock_runtime_client: BedrockRuntimeClient
|
|
39
|
+
aws_region: str = os.environ.get('AWS_REGION', 'us-east-1')
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if aws_profile := os.environ.get('AWS_PROFILE'):
|
|
43
|
+
bedrock_runtime_client = boto3.Session(
|
|
44
|
+
profile_name=aws_profile, region_name=aws_region
|
|
45
|
+
).client('bedrock-runtime')
|
|
46
|
+
else:
|
|
47
|
+
bedrock_runtime_client = boto3.Session(region_name=aws_region).client('bedrock-runtime')
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f'Error creating bedrock runtime client: {str(e)}')
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Create the MCP server Pwith detailed instructions
|
|
54
|
+
mcp = FastMCP(
|
|
55
|
+
'awslabs-nova-canvas-mcp-server',
|
|
56
|
+
instructions=f"""
|
|
57
|
+
# Amazon Nova Canvas Image Generation
|
|
58
|
+
|
|
59
|
+
This MCP server provides tools for generating images using Amazon Nova Canvas through Amazon Bedrock.
|
|
60
|
+
|
|
61
|
+
## Available Tools
|
|
62
|
+
|
|
63
|
+
### generate_image
|
|
64
|
+
Generate an image from a text prompt using Amazon Nova Canvas.
|
|
65
|
+
|
|
66
|
+
### generate_image_with_colors
|
|
67
|
+
Generate an image from a text prompt and color palette using Amazon Nova Canvas.
|
|
68
|
+
|
|
69
|
+
## Prompt Best Practices
|
|
70
|
+
|
|
71
|
+
{PROMPT_INSTRUCTIONS}
|
|
72
|
+
""",
|
|
73
|
+
dependencies=[
|
|
74
|
+
'pydantic',
|
|
75
|
+
'boto3',
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mcp.tool(name='generate_image')
|
|
81
|
+
async def mcp_generate_image(
|
|
82
|
+
ctx: Context,
|
|
83
|
+
prompt: str = Field(
|
|
84
|
+
description='The text description of the image to generate (1-1024 characters)'
|
|
85
|
+
),
|
|
86
|
+
negative_prompt: Optional[str] = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description='Text to define what not to include in the image (1-1024 characters)',
|
|
89
|
+
),
|
|
90
|
+
filename: Optional[str] = Field(
|
|
91
|
+
default=None, description='The name of the file to save the image to (without extension)'
|
|
92
|
+
),
|
|
93
|
+
width: int = Field(
|
|
94
|
+
default=DEFAULT_WIDTH,
|
|
95
|
+
description='The width of the generated image (320-4096, divisible by 16)',
|
|
96
|
+
),
|
|
97
|
+
height: int = Field(
|
|
98
|
+
default=DEFAULT_HEIGHT,
|
|
99
|
+
description='The height of the generated image (320-4096, divisible by 16)',
|
|
100
|
+
),
|
|
101
|
+
quality: str = Field(
|
|
102
|
+
default=DEFAULT_QUALITY,
|
|
103
|
+
description='The quality of the generated image ("standard" or "premium")',
|
|
104
|
+
),
|
|
105
|
+
cfg_scale: float = Field(
|
|
106
|
+
default=DEFAULT_CFG_SCALE,
|
|
107
|
+
description='How strongly the image adheres to the prompt (1.1-10.0)',
|
|
108
|
+
),
|
|
109
|
+
seed: Optional[int] = Field(default=None, description='Seed for generation (0-858,993,459)'),
|
|
110
|
+
number_of_images: int = Field(
|
|
111
|
+
default=DEFAULT_NUMBER_OF_IMAGES, description='The number of images to generate (1-5)'
|
|
112
|
+
),
|
|
113
|
+
workspace_dir: Optional[str] = Field(
|
|
114
|
+
default=None,
|
|
115
|
+
description="""The current workspace directory where the image should be saved.
|
|
116
|
+
CRITICAL: Assistant must always provide the current IDE workspace directory parameter to save images to the user's current project.""",
|
|
117
|
+
),
|
|
118
|
+
) -> McpImageGenerationResponse:
|
|
119
|
+
"""Generate an image using Amazon Nova Canvas with text prompt.
|
|
120
|
+
|
|
121
|
+
This tool uses Amazon Nova Canvas to generate images based on a text prompt.
|
|
122
|
+
The generated image will be saved to a file and the path will be returned.
|
|
123
|
+
|
|
124
|
+
IMPORTANT FOR ASSISTANT: Always send the current workspace directory when calling this tool!
|
|
125
|
+
The workspace_dir parameter should be set to the directory where the user is currently working
|
|
126
|
+
so that images are saved to a location accessible to the user.
|
|
127
|
+
|
|
128
|
+
## Prompt Best Practices
|
|
129
|
+
|
|
130
|
+
An effective prompt often includes short descriptions of:
|
|
131
|
+
1. The subject
|
|
132
|
+
2. The environment
|
|
133
|
+
3. (optional) The position or pose of the subject
|
|
134
|
+
4. (optional) Lighting description
|
|
135
|
+
5. (optional) Camera position/framing
|
|
136
|
+
6. (optional) The visual style or medium ("photo", "illustration", "painting", etc.)
|
|
137
|
+
|
|
138
|
+
Do not use negation words like "no", "not", "without" in your prompt. Instead, use the
|
|
139
|
+
negative_prompt parameter to specify what you don't want in the image.
|
|
140
|
+
|
|
141
|
+
You should always include "people, anatomy, hands, low quality, low resolution, low detail" in your negative_prompt
|
|
142
|
+
|
|
143
|
+
## Example Prompts
|
|
144
|
+
|
|
145
|
+
- "realistic editorial photo of female teacher standing at a blackboard with a warm smile"
|
|
146
|
+
- "whimsical and ethereal soft-shaded story illustration: A woman in a large hat stands at the ship's railing looking out across the ocean"
|
|
147
|
+
- "drone view of a dark river winding through a stark Iceland landscape, cinematic quality"
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
McpImageGenerationResponse: A response containing the generated image paths.
|
|
151
|
+
"""
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"MCP tool generate_image called with prompt: '{prompt[:30]}...', dims: {width}x{height}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
logger.info(
|
|
158
|
+
f'Generating image with text prompt, quality: {quality}, cfg_scale: {cfg_scale}'
|
|
159
|
+
)
|
|
160
|
+
response = await generate_image_with_text(
|
|
161
|
+
prompt=prompt,
|
|
162
|
+
bedrock_runtime_client=bedrock_runtime_client,
|
|
163
|
+
negative_prompt=negative_prompt,
|
|
164
|
+
filename=filename,
|
|
165
|
+
width=width,
|
|
166
|
+
height=height,
|
|
167
|
+
quality=quality,
|
|
168
|
+
cfg_scale=cfg_scale,
|
|
169
|
+
seed=seed,
|
|
170
|
+
number_of_images=number_of_images,
|
|
171
|
+
workspace_dir=workspace_dir,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if response.status == 'success':
|
|
175
|
+
# return response.paths
|
|
176
|
+
return McpImageGenerationResponse(
|
|
177
|
+
status='success',
|
|
178
|
+
paths=[f'file://{path}' for path in response.paths],
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
logger.error(f'Image generation returned error status: {response.message}')
|
|
182
|
+
await ctx.error(f'Failed to generate image: {response.message}') # type: ignore
|
|
183
|
+
# Return empty image or raise exception based on requirements
|
|
184
|
+
raise Exception(f'Failed to generate image: {response.message}')
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f'Error in mcp_generate_image: {str(e)}')
|
|
187
|
+
await ctx.error(f'Error generating image: {str(e)}') # type: ignore
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool(name='generate_image_with_colors')
|
|
192
|
+
async def mcp_generate_image_with_colors(
|
|
193
|
+
ctx: Context,
|
|
194
|
+
prompt: str = Field(
|
|
195
|
+
description='The text description of the image to generate (1-1024 characters)'
|
|
196
|
+
),
|
|
197
|
+
colors: List[str] = Field(
|
|
198
|
+
description='List of up to 10 hexadecimal color values (e.g., "#FF9800")'
|
|
199
|
+
),
|
|
200
|
+
negative_prompt: Optional[str] = Field(
|
|
201
|
+
default=None,
|
|
202
|
+
description='Text to define what not to include in the image (1-1024 characters)',
|
|
203
|
+
),
|
|
204
|
+
filename: Optional[str] = Field(
|
|
205
|
+
default=None, description='The name of the file to save the image to (without extension)'
|
|
206
|
+
),
|
|
207
|
+
width: int = Field(
|
|
208
|
+
default=1024, description='The width of the generated image (320-4096, divisible by 16)'
|
|
209
|
+
),
|
|
210
|
+
height: int = Field(
|
|
211
|
+
default=1024, description='The height of the generated image (320-4096, divisible by 16)'
|
|
212
|
+
),
|
|
213
|
+
quality: str = Field(
|
|
214
|
+
default='standard',
|
|
215
|
+
description='The quality of the generated image ("standard" or "premium")',
|
|
216
|
+
),
|
|
217
|
+
cfg_scale: float = Field(
|
|
218
|
+
default=6.5, description='How strongly the image adheres to the prompt (1.1-10.0)'
|
|
219
|
+
),
|
|
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
|
+
workspace_dir: Optional[str] = Field(
|
|
223
|
+
default=None,
|
|
224
|
+
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.",
|
|
225
|
+
),
|
|
226
|
+
) -> McpImageGenerationResponse:
|
|
227
|
+
"""Generate an image using Amazon Nova Canvas with color guidance.
|
|
228
|
+
|
|
229
|
+
This tool uses Amazon Nova Canvas to generate images based on a text prompt and color palette.
|
|
230
|
+
The generated image will be saved to a file and the path will be returned.
|
|
231
|
+
|
|
232
|
+
IMPORTANT FOR Assistant: Always send the current workspace directory when calling this tool!
|
|
233
|
+
The workspace_dir parameter should be set to the directory where the user is currently working
|
|
234
|
+
so that images are saved to a location accessible to the user.
|
|
235
|
+
|
|
236
|
+
## Prompt Best Practices
|
|
237
|
+
|
|
238
|
+
An effective prompt often includes short descriptions of:
|
|
239
|
+
1. The subject
|
|
240
|
+
2. The environment
|
|
241
|
+
3. (optional) The position or pose of the subject
|
|
242
|
+
4. (optional) Lighting description
|
|
243
|
+
5. (optional) Camera position/framing
|
|
244
|
+
6. (optional) The visual style or medium ("photo", "illustration", "painting", etc.)
|
|
245
|
+
|
|
246
|
+
Do not use negation words like "no", "not", "without" in your prompt. Instead, use the
|
|
247
|
+
negative_prompt parameter to specify what you don't want in the image.
|
|
248
|
+
|
|
249
|
+
## Example Colors
|
|
250
|
+
|
|
251
|
+
- ["#FF5733", "#33FF57", "#3357FF"] - A vibrant color scheme with red, green, and blue
|
|
252
|
+
- ["#000000", "#FFFFFF"] - A high contrast black and white scheme
|
|
253
|
+
- ["#FFD700", "#B87333"] - A gold and bronze color scheme
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
McpImageGenerationResponse: A response containing the generated image paths.
|
|
257
|
+
"""
|
|
258
|
+
logger.debug(
|
|
259
|
+
f"MCP tool generate_image_with_colors called with prompt: '{prompt[:30]}...', {len(colors)} colors"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
color_hex_list = ', '.join(colors[:3]) + (', ...' if len(colors) > 3 else '')
|
|
264
|
+
logger.info(
|
|
265
|
+
f'Generating color-guided image with colors: [{color_hex_list}], quality: {quality}'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
response = await generate_image_with_colors(
|
|
269
|
+
prompt=prompt,
|
|
270
|
+
colors=colors,
|
|
271
|
+
bedrock_runtime_client=bedrock_runtime_client,
|
|
272
|
+
negative_prompt=negative_prompt,
|
|
273
|
+
filename=filename,
|
|
274
|
+
width=width,
|
|
275
|
+
height=height,
|
|
276
|
+
quality=quality,
|
|
277
|
+
cfg_scale=cfg_scale,
|
|
278
|
+
seed=seed,
|
|
279
|
+
number_of_images=number_of_images,
|
|
280
|
+
workspace_dir=workspace_dir,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if response.status == 'success':
|
|
284
|
+
return McpImageGenerationResponse(
|
|
285
|
+
status='success',
|
|
286
|
+
paths=[f'file://{path}' for path in response.paths],
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
logger.error(
|
|
290
|
+
f'Color-guided image generation returned error status: {response.message}'
|
|
291
|
+
)
|
|
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
|
+
except Exception as e:
|
|
295
|
+
logger.error(f'Error in mcp_generate_image_with_colors: {str(e)}')
|
|
296
|
+
await ctx.error(f'Error generating color-guided image: {str(e)}')
|
|
297
|
+
raise
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main():
|
|
301
|
+
"""Run the MCP server with CLI argument support."""
|
|
302
|
+
logger.info('Starting nova-canvas-mcp-server MCP server')
|
|
303
|
+
|
|
304
|
+
parser = argparse.ArgumentParser(
|
|
305
|
+
description='MCP server for generating images using Amazon Nova Canvas'
|
|
306
|
+
)
|
|
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
|
+
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
logger.debug(f'Parsed arguments: sse={args.sse}, port={args.port}')
|
|
312
|
+
|
|
313
|
+
# Run server with appropriate transport
|
|
314
|
+
if args.sse:
|
|
315
|
+
logger.info(f'Using SSE transport on port {args.port}')
|
|
316
|
+
mcp.settings.port = args.port
|
|
317
|
+
mcp.run(transport='sse')
|
|
318
|
+
else:
|
|
319
|
+
logger.info('Using standard stdio transport')
|
|
320
|
+
mcp.run()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == '__main__':
|
|
324
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: awslabs.nova-canvas-mcp-server
|
|
3
|
+
Version: 0.1.10233
|
|
4
|
+
Summary: An AWS Labs Model Context Protocol (MCP) server for Amazon Nova Canvas
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: boto3>=1.37.24
|
|
7
|
+
Requires-Dist: loguru>=0.7.3
|
|
8
|
+
Requires-Dist: mcp[cli]>=1.6.0
|
|
9
|
+
Requires-Dist: pydantic>=2.11.1
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# awslabs MCP Nova Canvas Server
|
|
13
|
+
|
|
14
|
+
MCP server for generating images using Amazon Nova Canvas
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Text-based image generation** - Create images from text prompts with `generate_image`
|
|
19
|
+
- Customizable dimensions (320-4096px), quality options, and negative prompting
|
|
20
|
+
- Supports multiple image generation (1-5) in single request
|
|
21
|
+
- Adjustable parameters like cfg_scale (1.1-10.0) and seeded generation
|
|
22
|
+
|
|
23
|
+
- **Color-guided image generation** - Generate images with specific color palettes using `generate_image_with_colors`
|
|
24
|
+
- Define up to 10 hex color values to influence the image style and mood
|
|
25
|
+
- Same customization options as text-based generation
|
|
26
|
+
|
|
27
|
+
- **Workspace integration** - Images saved to user-specified workspace directories with automatic folder creation
|
|
28
|
+
|
|
29
|
+
- **AWS authentication** - Uses AWS profiles for secure access to Amazon Nova Canvas services
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
1. Install `uv` from [Astral](https://docs.astral.sh/uv/getting-started/installation/) or the [GitHub README](https://github.com/astral-sh/uv#installation)
|
|
34
|
+
2. Install Python using `uv python install 3.13`
|
|
35
|
+
3. Set up AWS credentials with access to Amazon Bedrock and Nova Canvas
|
|
36
|
+
- You need an AWS account with Amazon Bedrock and Amazon Nova Canvas enabled
|
|
37
|
+
- Configure AWS credentials with `aws configure` or environment variables
|
|
38
|
+
- Ensure your IAM role/user has permissions to use Amazon Bedrock and Nova Canvas
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Install the MCP server:
|
|
43
|
+
|
|
44
|
+
Add the server to your MCP client config (e.g. for Amazon Q CLI MCP, `~/.aws/amazonq/mcp.json`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"awslabs.nova-canvas-mcp-server": {
|
|
50
|
+
"command": "uvx",
|
|
51
|
+
"args": ["awslabs.nova-canvas-mcp-server@latest"],
|
|
52
|
+
"env": {
|
|
53
|
+
"AWS_PROFILE": "your-aws-profile", // Optional: specify AWS profile
|
|
54
|
+
"AWS_REGION": "us-east-1" // Required: region where Bedrock is available
|
|
55
|
+
},
|
|
56
|
+
"disabled": false,
|
|
57
|
+
"autoApprove": []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### AWS Authentication
|
|
64
|
+
|
|
65
|
+
The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment variable. If not provided, it defaults to the "default" profile in your AWS configuration file.
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
"env": {
|
|
69
|
+
"AWS_PROFILE": "your-aws-profile", // Specify which AWS profile to use
|
|
70
|
+
"AWS_REGION": "us-east-1" // Region where Bedrock is available
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Make sure the AWS profile has permissions to access Amazon Bedrock and Amazon Nova Canvas. The MCP server creates a boto3 session using the specified profile to authenticate with AWS services. Your AWS IAM credentials remain on your local machine and are strictly used for using the Amazon Bedrock model APIs.
|
|
@@ -0,0 +1,10 @@
|
|
|
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,,
|