skydeckai-code 0.1.23__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.
@@ -0,0 +1,127 @@
1
+ import base64
2
+ import io
3
+ import os
4
+
5
+ from mcp.types import TextContent
6
+ from PIL import Image
7
+
8
+ from .state import state
9
+
10
+ # Maximum file size (100MB)
11
+ MAX_FILE_SIZE = 100 * 1024 * 1024
12
+
13
+ # Image size constraints
14
+ MIN_WIDTH = 20
15
+ MAX_WIDTH = 800
16
+
17
+ def read_image_file_tool():
18
+ return {
19
+ "name": "read_image_file",
20
+ "description": "Read an image file from the file system and return its contents as a base64-encoded string. "
21
+ "WHEN TO USE: When you need to view or process image files, include images in responses, analyze "
22
+ "image content, or convert images to a format that can be transmitted as text. Useful for examining "
23
+ "screenshots, diagrams, photos, or any visual content stored in the file system. "
24
+ "WHEN NOT TO USE: When you only need information about the image file without its contents "
25
+ "(use get_file_info instead), when working with extremely large images (over 100MB), or when you "
26
+ "need to read text files (use read_file instead). "
27
+ "RETURNS: A base64-encoded data URI string prefixed with the appropriate MIME type "
28
+ "(e.g., 'data:image/png;base64,...'). Images that are very small or very large will be automatically "
29
+ "resized to between 20-800 pixels wide while maintaining aspect ratio. This tool supports common image "
30
+ "formats like PNG, JPEG, GIF, and WebP. Only works within the allowed directory.",
31
+ "inputSchema": {
32
+ "type": "object",
33
+ "properties": {
34
+ "path": {
35
+ "type": "string",
36
+ "description": "Path to the image file to read. This must be a valid image file in a supported format "
37
+ "(PNG, JPEG, GIF, WebP). Examples: 'screenshots/screen.png', 'images/logo.jpg', "
38
+ "'diagrams/flowchart.gif'. Both absolute and relative paths are supported, but must be "
39
+ "within the allowed workspace."
40
+ },
41
+ "max_size": {
42
+ "type": "integer",
43
+ "description": "Maximum file size in bytes to allow. Files larger than this size will be rejected to "
44
+ "prevent memory issues. Default is 100MB (104,857,600 bytes). For most use cases, the "
45
+ "default value is sufficient, but you can lower this when working with limited memory.",
46
+ "optional": True
47
+ }
48
+ },
49
+ "required": ["path"]
50
+ },
51
+ }
52
+
53
+ async def handle_read_image_file(arguments: dict):
54
+ """Handle reading an image file and converting it to base64."""
55
+ path = arguments.get("path")
56
+ max_size = arguments.get("max_size", MAX_FILE_SIZE)
57
+
58
+ if not path:
59
+ raise ValueError("path must be provided")
60
+
61
+ # Determine full path based on whether input is absolute or relative
62
+ if os.path.isabs(path):
63
+ full_path = os.path.abspath(path) # Just normalize the absolute path
64
+ else:
65
+ # For relative paths, join with allowed_directory
66
+ full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
67
+
68
+ if not full_path.startswith(state.allowed_directory):
69
+ raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})")
70
+
71
+ if not os.path.exists(full_path):
72
+ raise ValueError(f"File does not exist: {full_path}")
73
+ if not os.path.isfile(full_path):
74
+ raise ValueError(f"Path is not a file: {full_path}")
75
+
76
+ # Check file size before attempting to read
77
+ file_size = os.path.getsize(full_path)
78
+ if file_size > max_size:
79
+ raise ValueError(f"File size ({file_size} bytes) exceeds maximum allowed size ({max_size} bytes)")
80
+
81
+ try:
82
+ # Try to open the image with PIL to validate it's a valid image
83
+ with Image.open(full_path) as img:
84
+ # Get the image format
85
+ image_format = img.format.lower()
86
+ if not image_format:
87
+ # Try to determine format from file extension
88
+ ext = os.path.splitext(full_path)[1].lower().lstrip('.')
89
+ if ext in ['jpg', 'jpeg']:
90
+ image_format = 'jpeg'
91
+ elif ext in ['png', 'gif', 'webp']:
92
+ image_format = ext
93
+ else:
94
+ raise ValueError(f"Unsupported image format: {ext}")
95
+
96
+ # Resize image if width is greater than MAX_WIDTH or less than MIN_WIDTH
97
+ if img.width > MAX_WIDTH or img.width < MIN_WIDTH:
98
+ # Calculate new dimensions maintaining aspect ratio
99
+ if img.width > MAX_WIDTH:
100
+ target_width = MAX_WIDTH
101
+ else:
102
+ target_width = MIN_WIDTH
103
+
104
+ ratio = target_width / img.width
105
+ new_height = int(img.height * ratio)
106
+ img = img.resize((target_width, new_height), Image.Resampling.LANCZOS)
107
+
108
+ # Convert image to bytes
109
+ img_byte_arr = io.BytesIO()
110
+ if image_format.lower() == 'jpeg':
111
+ img.save(img_byte_arr, format=image_format, quality=85) # Specify quality for JPEG
112
+ else:
113
+ img.save(img_byte_arr, format=image_format)
114
+ img_byte_arr = img_byte_arr.getvalue()
115
+
116
+ # Convert to base64
117
+ base64_data = base64.b64encode(img_byte_arr).decode('utf-8')
118
+
119
+ # Return the image data with its type
120
+ return [TextContent(
121
+ type="text",
122
+ text=f"data:image/{image_format};base64,{base64_data}"
123
+ )]
124
+ except Image.UnidentifiedImageError:
125
+ raise ValueError(f"File is not a valid image: {path}")
126
+ except Exception as e:
127
+ raise ValueError(f"Error reading image file: {str(e)}")
@@ -0,0 +1,86 @@
1
+ import os
2
+
3
+ from mcp.types import TextContent
4
+ from .state import state
5
+
6
+
7
+ def get_allowed_directory_tool():
8
+ """Define the get_allowed_directory tool."""
9
+ return {
10
+ "name": "get_allowed_directory",
11
+ "description": "Get the current working directory that this server is allowed to access. "
12
+ "WHEN TO USE: When you need to understand the current workspace boundaries, determine "
13
+ "the root directory for relative paths, or verify where file operations are permitted. "
14
+ "Useful for commands that need to know the allowed workspace root. "
15
+ "WHEN NOT TO USE: When you already know the current working directory or when you need "
16
+ "to actually list files in the directory (use directory_listing instead). "
17
+ "RETURNS: A string containing the absolute path to the current allowed working directory. "
18
+ "This is the root directory within which all file operations must occur.",
19
+ "inputSchema": {
20
+ "type": "object",
21
+ "properties": {},
22
+ "required": [],
23
+ },
24
+ }
25
+
26
+ def update_allowed_directory_tool():
27
+ """Define the update_allowed_directory tool."""
28
+ return {
29
+ "name": "update_allowed_directory",
30
+ "description": "Change the working directory that this server is allowed to access. "
31
+ "WHEN TO USE: When you need to switch between different projects, change the workspace "
32
+ "root to a different directory, or expand/modify the boundaries of allowed file operations. "
33
+ "Useful when working with multiple projects or repositories in different locations. "
34
+ "WHEN NOT TO USE: When you only need to create a subdirectory within the current workspace "
35
+ "(use create_directory instead), or when you just want to list files in a different directory "
36
+ "(use directory_listing instead). "
37
+ "RETURNS: A confirmation message indicating that the allowed directory has been successfully "
38
+ "updated to the new path.",
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "directory": {
43
+ "type": "string",
44
+ "description": "Directory to allow access to. Must be an absolute path that exists on the system. "
45
+ "Use ~ to refer to the user's home directory. Examples: '/Users/username/projects', "
46
+ "'~/Documents/code', '/home/user/repositories'. The directory must exist and be "
47
+ "accessible to the user running the application."
48
+ }
49
+ },
50
+ "required": ["directory"]
51
+ },
52
+ }
53
+
54
+ async def handle_get_allowed_directory(arguments: dict):
55
+ """Handle getting the allowed directory."""
56
+ return [TextContent(
57
+ type="text",
58
+ text=f"Allowed directory: {state.allowed_directory}"
59
+ )]
60
+
61
+ async def handle_update_allowed_directory(arguments: dict):
62
+ """Handle updating the allowed directory."""
63
+ directory = arguments.get("directory")
64
+ if not directory:
65
+ raise ValueError("directory must be provided")
66
+
67
+ # Handle home directory expansion
68
+ if directory.startswith("~"):
69
+ directory = os.path.expanduser(directory)
70
+
71
+ # Must be an absolute path
72
+ if not os.path.isabs(directory):
73
+ raise ValueError("Directory must be an absolute path")
74
+
75
+ # Normalize the path
76
+ directory = os.path.abspath(directory)
77
+
78
+ # Verify directory exists
79
+ if not os.path.isdir(directory):
80
+ raise ValueError(f"Path is not a directory: {directory}")
81
+
82
+ state.allowed_directory = directory
83
+ return [TextContent(
84
+ type="text",
85
+ text=f"Successfully updated allowed directory to: {state.allowed_directory}"
86
+ )]