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