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.
@@ -1,3 +1,3 @@
1
1
  """awslabs.nova-canvas-mcp-server"""
2
2
 
3
- __version__ = '0.1.6'
3
+ __version__ = "0.1.6"
@@ -1,11 +1,11 @@
1
1
  # Constants
2
- NOVA_CANVAS_MODEL_ID = 'amazon.nova-canvas-v1:0'
2
+ NOVA_CANVAS_MODEL_ID = "amazon.nova-canvas-v1:0"
3
3
  DEFAULT_WIDTH = 1024
4
4
  DEFAULT_HEIGHT = 1024
5
- DEFAULT_QUALITY = 'standard'
5
+ DEFAULT_QUALITY = "standard"
6
6
  DEFAULT_CFG_SCALE = 6.5
7
7
  DEFAULT_NUMBER_OF_IMAGES = 1
8
- DEFAULT_OUTPUT_DIR = 'output' # Default directory inside workspace_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 = 'standard'
19
- PREMIUM = '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 = 'TEXT_IMAGE'
31
- COLOR_GUIDED_GENERATION = '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(default_factory=lambda: random.randint(0, 858993459), ge=0, le=858993459)
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('width', 'height')
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('Value must be divisible by 16')
73
+ raise ValueError("Value must be divisible by 16")
72
74
  return v
73
75
 
74
- @model_validator(mode='after')
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('Aspect ratio must be between 1:4 and 4:1')
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('Total pixel count must be less than 4,194,304')
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('colors')
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'^#[0-9A-Fa-f]{6}$')
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('negativeText') is None:
184
- text_to_image_params.pop('negativeText', None)
185
+ if text_to_image_params.get("negativeText") is None:
186
+ text_to_image_params.pop("negativeText", None)
185
187
 
186
188
  return {
187
- 'taskType': self.taskType,
188
- 'textToImageParams': text_to_image_params,
189
- 'imageGenerationConfig': self.imageGenerationConfig.model_dump()
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[TaskType.COLOR_GUIDED_GENERATION] = TaskType.COLOR_GUIDED_GENERATION
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('negativeText') is None:
223
- color_guided_params.pop('negativeText', None)
226
+ if color_guided_params.get("negativeText") is None:
227
+ color_guided_params.pop("negativeText", None)
224
228
 
225
229
  return {
226
- 'taskType': self.taskType,
227
- 'colorGuidedGenerationParams': color_guided_params,
228
- 'imageGenerationConfig': self.imageGenerationConfig.model_dump()
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 = 'nova_canvas',
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'Saving {len(base64_images)} images')
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'{filename}_{i + 1}.png' if number_of_images > 1 else f'{filename}.png'
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 = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))
81
- image_filename = f'{prefix}_{random_id}_{i + 1}.png'
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, 'wb') as file:
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 {'paths': saved_paths}
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('Invoking Nova Canvas API')
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'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
+ 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['body'].read())
125
- logger.info('Nova Canvas API call successful')
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'Nova Canvas API call failed: {str(e)}')
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(f"Generating text-to-image with prompt: '{prompt[:30]}...' ({width}x{height})")
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('Validating parameters and creating request model')
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 if quality == DEFAULT_QUALITY else Quality.PREMIUM,
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(text=prompt, negativeText=negative_prompt)
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('Request validation successful')
209
+ logger.info("Request validation successful")
200
210
 
201
211
  except Exception as e:
202
- logger.error(f'Parameter validation failed: {str(e)}')
212
+ logger.error(f"Parameter validation failed: {str(e)}")
203
213
  return ImageGenerationResponse(
204
- status='error',
205
- message=f'Validation error: {str(e)}',
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('Sending request to Nova Canvas API')
214
- model_response = await invoke_nova_canvas(request_model_dict, bedrock_runtime_client)
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['images']
218
- logger.info(f'Received {len(base64_images)} images from Nova Canvas API')
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='nova_canvas',
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='success',
243
+ status="success",
232
244
  message=f'Generated {len(result["paths"])} image(s)',
233
- paths=result['paths'],
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'Image generation failed: {str(e)}')
250
+ logger.error(f"Image generation failed: {str(e)}")
239
251
  return ImageGenerationResponse(
240
- status='error',
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'Unexpected error in generate_image_with_text: {str(e)}')
260
+ logger.error(f"Unexpected error in generate_image_with_text: {str(e)}")
249
261
  return ImageGenerationResponse(
250
- status='error',
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('Validating parameters and creating color-guided request model')
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 if quality == DEFAULT_QUALITY else Quality.PREMIUM,
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('Color-guided request validation successful')
352
+ logger.info("Color-guided request validation successful")
337
353
 
338
354
  except Exception as e:
339
- logger.error(f'Color-guided parameter validation failed: {str(e)}')
355
+ logger.error(f"Color-guided parameter validation failed: {str(e)}")
340
356
  return ImageGenerationResponse(
341
- status='error',
342
- message=f'Validation error: {str(e)}',
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('Sending color-guided request to Nova Canvas API')
352
- model_response = await invoke_nova_canvas(request_model_dict, bedrock_runtime_client)
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['images']
356
- logger.info(f'Received {len(base64_images)} images from Nova Canvas API')
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='nova_canvas_color',
381
+ prefix="nova_canvas_color",
364
382
  workspace_dir=workspace_dir,
365
383
  )
366
384
 
367
- logger.info(f'Successfully generated {len(result["paths"])} color-guided image(s)')
385
+ logger.info(
386
+ f'Successfully generated {len(result["paths"])} color-guided image(s)'
387
+ )
368
388
  return ImageGenerationResponse(
369
- status='success',
389
+ status="success",
370
390
  message=f'Generated {len(result["paths"])} image(s)',
371
- paths=result['paths'],
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'Color-guided image generation failed: {str(e)}')
397
+ logger.error(f"Color-guided image generation failed: {str(e)}")
378
398
  return ImageGenerationResponse(
379
- status='error',
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'Unexpected error in generate_image_with_colors: {str(e)}')
408
+ logger.error(f"Unexpected error in generate_image_with_colors: {str(e)}")
389
409
  return ImageGenerationResponse(
390
- status='error',
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('FASTMCP_LOG_LEVEL', 'WARNING'))
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('AWS_REGION', 'us-east-1')
39
+ aws_region: str = os.environ.get("AWS_REGION", "us-east-1")
40
40
 
41
41
  try:
42
- if aws_profile := os.environ.get('AWS_PROFILE'):
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('bedrock-runtime')
45
+ ).client("bedrock-runtime")
46
46
  else:
47
- bedrock_runtime_client = boto3.Session(region_name=aws_region).client('bedrock-runtime')
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'Error creating bedrock runtime client: {str(e)}')
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
- 'awslabs-nova-canvas-mcp-server',
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
- 'pydantic',
75
- 'boto3',
76
+ "pydantic",
77
+ "boto3",
76
78
  ],
77
79
  )
78
80
 
79
81
 
80
- @mcp.tool(name='generate_image')
82
+ @mcp.tool(name="generate_image")
81
83
  async def mcp_generate_image(
82
84
  ctx: Context,
83
85
  prompt: str = Field(
84
- description='The text description of the image to generate (1-1024 characters)'
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='Text to define what not to include in the image (1-1024 characters)',
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, description='The name of the file to save the image to (without extension)'
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='The width of the generated image (320-4096, divisible by 16)',
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='The height of the generated image (320-4096, divisible by 16)',
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='How strongly the image adheres to the prompt (1.1-10.0)',
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, description='The number of images to generate (1-5)'
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'Generating image with text prompt, quality: {quality}, cfg_scale: {cfg_scale}'
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 == 'success':
180
+ if response.status == "success":
175
181
  # return response.paths
176
182
  return McpImageGenerationResponse(
177
- status='success',
178
- paths=[f'file://{path}' for path in response.paths],
183
+ status="success",
184
+ paths=[f"file://{path}" for path in response.paths],
179
185
  )
180
186
  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
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'Failed to generate image: {response.message}')
190
+ raise Exception(f"Failed to generate image: {response.message}")
185
191
  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
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='generate_image_with_colors')
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='The text description of the image to generate (1-1024 characters)'
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='Text to define what not to include in the image (1-1024 characters)',
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, description='The name of the file to save the image to (without extension)'
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, description='The width of the generated image (320-4096, divisible by 16)'
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, description='The height of the generated image (320-4096, divisible by 16)'
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='standard',
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, description='How strongly the image adheres to the prompt (1.1-10.0)'
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 = ', '.join(colors[:3]) + (', ...' if len(colors) > 3 else '')
277
+ color_hex_list = ", ".join(colors[:3]) + (", ..." if len(colors) > 3 else "")
264
278
  logger.info(
265
- f'Generating color-guided image with colors: [{color_hex_list}], quality: {quality}'
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 == 'success':
297
+ if response.status == "success":
284
298
  return McpImageGenerationResponse(
285
- status='success',
286
- paths=[f'file://{path}' for path in response.paths],
299
+ status="success",
300
+ paths=[f"file://{path}" for path in response.paths],
287
301
  )
288
302
  else:
289
303
  logger.error(
290
- f'Color-guided image generation returned error status: {response.message}'
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'Error in mcp_generate_image_with_colors: {str(e)}')
296
- await ctx.error(f'Error generating color-guided image: {str(e)}')
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('Starting nova-canvas-mcp-server MCP server')
320
+ logger.info("Starting nova-canvas-mcp-server MCP server")
303
321
 
304
322
  parser = argparse.ArgumentParser(
305
- description='MCP server for generating images using Amazon Nova Canvas'
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'Parsed arguments: sse={args.sse}, port={args.port}')
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'Using SSE transport on port {args.port}')
335
+ logger.info(f"Using SSE transport on port {args.port}")
316
336
  mcp.settings.port = args.port
317
- mcp.run(transport='sse')
337
+ mcp.run(transport="sse")
318
338
  else:
319
- logger.info('Using standard stdio transport')
339
+ logger.info("Using standard stdio transport")
320
340
  mcp.run()
321
341
 
322
342
 
323
- if __name__ == '__main__':
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.10233
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
- # awslabs MCP Nova Canvas Server
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,,