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 ADDED
@@ -0,0 +1,2 @@
1
+ # This file is part of the awslabs namespace.
2
+ # It is intentionally minimal to support PEP 420 namespace packages.
@@ -0,0 +1,3 @@
1
+ """awslabs.nova-canvas-mcp-server"""
2
+
3
+ __version__ = '0.1.6'
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ awslabs.nova-canvas-mcp-server = awslabs.nova_canvas_mcp_server.server:main