Merge branch 'main' of https://huggingface.co/spaces/AgentsGuards/agents-guard-mcp
Browse files- mcp_server.py +5 -1
- src/utils/compress.py +49 -0
- src/utils/describe.py +109 -0
- src/utils/{image_helpers.py → remove_background.py} +0 -0
mcp_server.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1 |
from mcp.server.fastmcp import FastMCP
|
2 |
from src.utils.change_format import change_format
|
3 |
-
from src.utils.
|
4 |
from src.utils.visualize_image import visualize_base64_image
|
5 |
from src.utils.generate_image import generate_image
|
6 |
from src.utils.apply_filter import apply_filter
|
7 |
from src.utils.add_text import add_text_to_image
|
8 |
from src.utils.watermark import add_watermark, remove_watermark
|
|
|
|
|
9 |
|
10 |
mcp = FastMCP("Youtube Service")
|
11 |
|
12 |
mcp.add_tool(remove_background_from_url)
|
|
|
13 |
mcp.add_tool(change_format)
|
14 |
mcp.add_tool(visualize_base64_image)
|
15 |
mcp.add_tool(generate_image)
|
@@ -17,6 +20,7 @@ mcp.add_tool(apply_filter)
|
|
17 |
mcp.add_tool(add_text_to_image)
|
18 |
mcp.add_tool(add_watermark)
|
19 |
mcp.add_tool(remove_watermark)
|
|
|
20 |
|
21 |
if __name__ == "__main__":
|
22 |
mcp.run()
|
|
|
1 |
from mcp.server.fastmcp import FastMCP
|
2 |
from src.utils.change_format import change_format
|
3 |
+
from src.utils.remove_background import remove_background_from_url
|
4 |
from src.utils.visualize_image import visualize_base64_image
|
5 |
from src.utils.generate_image import generate_image
|
6 |
from src.utils.apply_filter import apply_filter
|
7 |
from src.utils.add_text import add_text_to_image
|
8 |
from src.utils.watermark import add_watermark, remove_watermark
|
9 |
+
from src.utils.describe import describe_image
|
10 |
+
from src.utils.compress import compress_image
|
11 |
|
12 |
mcp = FastMCP("Youtube Service")
|
13 |
|
14 |
mcp.add_tool(remove_background_from_url)
|
15 |
+
mcp.add_tool(describe_image)
|
16 |
mcp.add_tool(change_format)
|
17 |
mcp.add_tool(visualize_base64_image)
|
18 |
mcp.add_tool(generate_image)
|
|
|
20 |
mcp.add_tool(add_text_to_image)
|
21 |
mcp.add_tool(add_watermark)
|
22 |
mcp.add_tool(remove_watermark)
|
23 |
+
mcp.add_tool(compress_image)
|
24 |
|
25 |
if __name__ == "__main__":
|
26 |
mcp.run()
|
src/utils/compress.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import Image
|
2 |
+
import os
|
3 |
+
from typing import Literal, Optional
|
4 |
+
|
5 |
+
def compress_image(
|
6 |
+
input_path: str,
|
7 |
+
output_path: str,
|
8 |
+
quality: int = 85,
|
9 |
+
format: Literal["JPEG", "PNG", "WEBP"] = "JPEG",
|
10 |
+
max_width: Optional[int] = None,
|
11 |
+
max_height: Optional[int] = None
|
12 |
+
) -> str:
|
13 |
+
"""
|
14 |
+
Compress an image file.
|
15 |
+
|
16 |
+
Args:
|
17 |
+
input_path: Path to input image
|
18 |
+
output_path: Path for compressed output
|
19 |
+
quality: Compression quality 1-95 (for JPEG/WEBP)
|
20 |
+
format: Output format
|
21 |
+
max_width: Maximum width (optional)
|
22 |
+
max_height: Maximum height (optional)
|
23 |
+
"""
|
24 |
+
try:
|
25 |
+
if not os.path.splitext(output_path)[1]:
|
26 |
+
extension_map = {"JPEG": ".jpg", "PNG": ".png", "WEBP": ".webp"}
|
27 |
+
output_path = output_path + extension_map[format]
|
28 |
+
|
29 |
+
with Image.open(input_path) as img:
|
30 |
+
if format == "JPEG" and img.mode in ("RGBA", "P"):
|
31 |
+
img = img.convert("RGB")
|
32 |
+
|
33 |
+
if max_width or max_height:
|
34 |
+
img.thumbnail((max_width or img.width, max_height or img.height), Image.Resampling.LANCZOS)
|
35 |
+
|
36 |
+
save_kwargs = {"format": format, "optimize": True}
|
37 |
+
if format in ["JPEG", "WEBP"]:
|
38 |
+
save_kwargs["quality"] = quality
|
39 |
+
|
40 |
+
img.save(output_path, **save_kwargs)
|
41 |
+
|
42 |
+
original_size = os.path.getsize(input_path) / 1024 / 1024
|
43 |
+
compressed_size = os.path.getsize(output_path) / 1024 / 1024
|
44 |
+
reduction = (1 - compressed_size/original_size) * 100
|
45 |
+
|
46 |
+
return f"✅ Compressed successfully!\nOriginal: {original_size:.2f}MB → Compressed: {compressed_size:.2f}MB\nReduction: {reduction:.1f}%"
|
47 |
+
|
48 |
+
except Exception as e:
|
49 |
+
return f"❌ Error: {str(e)}"
|
src/utils/describe.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import base64
|
3 |
+
import requests
|
4 |
+
from pathlib import Path
|
5 |
+
from openai import OpenAI
|
6 |
+
from urllib.parse import urlparse
|
7 |
+
|
8 |
+
def describe_image(image_path: str) -> str:
|
9 |
+
"""
|
10 |
+
Generate a description of the image at the given path or URL.
|
11 |
+
|
12 |
+
Args:
|
13 |
+
image_path: Path to local image file OR URL to image
|
14 |
+
|
15 |
+
Returns:
|
16 |
+
A string description of the image """
|
17 |
+
|
18 |
+
# Check if API key is available
|
19 |
+
api_key = os.getenv("NEBIUS_API_KEY")
|
20 |
+
if not api_key:
|
21 |
+
return "Error: NEBIUS_API_KEY environment variable not set"
|
22 |
+
|
23 |
+
try:
|
24 |
+
# Determine if it's a URL or local file path
|
25 |
+
parsed = urlparse(image_path)
|
26 |
+
is_url = bool(parsed.scheme and parsed.netloc)
|
27 |
+
|
28 |
+
if is_url:
|
29 |
+
# Handle URL
|
30 |
+
print(f"📡 Downloading image from URL: {image_path}")
|
31 |
+
response = requests.get(image_path, timeout=30)
|
32 |
+
response.raise_for_status()
|
33 |
+
image_data = response.content
|
34 |
+
|
35 |
+
# Determine content type from response headers
|
36 |
+
content_type = response.headers.get('content-type', '')
|
37 |
+
if 'image' not in content_type:
|
38 |
+
return f"Error: URL does not appear to contain an image. Content-Type: {content_type}"
|
39 |
+
|
40 |
+
else:
|
41 |
+
# Handle local file
|
42 |
+
image_path = Path(image_path)
|
43 |
+
|
44 |
+
if not image_path.exists():
|
45 |
+
return f"Error: Local file not found: {image_path}"
|
46 |
+
|
47 |
+
# Check if it's an image file
|
48 |
+
valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
|
49 |
+
if image_path.suffix.lower() not in valid_extensions:
|
50 |
+
return f"Error: Unsupported file type '{image_path.suffix}'. Supported: {valid_extensions}"
|
51 |
+
|
52 |
+
print(f"📁 Reading local image: {image_path}")
|
53 |
+
with open(image_path, "rb") as f:
|
54 |
+
image_data = f.read()
|
55 |
+
|
56 |
+
# Encode image to base64
|
57 |
+
base64_image = base64.b64encode(image_data).decode('utf-8')
|
58 |
+
|
59 |
+
# Create OpenAI client
|
60 |
+
client = OpenAI(
|
61 |
+
base_url="https://api.studio.nebius.com/v1/",
|
62 |
+
api_key=api_key
|
63 |
+
)
|
64 |
+
|
65 |
+
# Make API call with proper vision format
|
66 |
+
response = client.chat.completions.create(
|
67 |
+
model="mistralai/Mistral-Small-3.1-24B-Instruct-2503",
|
68 |
+
messages=[
|
69 |
+
{
|
70 |
+
"role": "system",
|
71 |
+
"content": "You are a helpful assistant that provides detailed descriptions of images. Focus on the main subjects, colors, composition, and any notable details."
|
72 |
+
},
|
73 |
+
{
|
74 |
+
"role": "user",
|
75 |
+
"content": [
|
76 |
+
{
|
77 |
+
"type": "text",
|
78 |
+
"text": "Please provide a detailed description of this image."
|
79 |
+
},
|
80 |
+
{
|
81 |
+
"type": "image_url",
|
82 |
+
"image_url": {
|
83 |
+
"url": f"data:image/jpeg;base64,{base64_image}"
|
84 |
+
}
|
85 |
+
}
|
86 |
+
]
|
87 |
+
}
|
88 |
+
],
|
89 |
+
max_tokens=500
|
90 |
+
)
|
91 |
+
|
92 |
+
description = response.choices[0].message.content.strip()
|
93 |
+
return description
|
94 |
+
|
95 |
+
except requests.RequestException as e:
|
96 |
+
return f"Error downloading image from URL: {str(e)}"
|
97 |
+
except FileNotFoundError:
|
98 |
+
return f"Error: File not found: {image_path}"
|
99 |
+
except Exception as e:
|
100 |
+
error_msg = str(e)
|
101 |
+
|
102 |
+
if "vision" in error_msg.lower() or "image" in error_msg.lower():
|
103 |
+
return f"Error: This model may not support vision capabilities. Try a vision-enabled model. Details: {error_msg}"
|
104 |
+
elif "401" in error_msg or "unauthorized" in error_msg.lower():
|
105 |
+
return "Error: Invalid API key or insufficient permissions"
|
106 |
+
elif "rate" in error_msg.lower() or "quota" in error_msg.lower():
|
107 |
+
return f"Error: API rate limit or quota exceeded: {error_msg}"
|
108 |
+
else:
|
109 |
+
return f"Error processing image: {error_msg}"
|
src/utils/{image_helpers.py → remove_background.py}
RENAMED
File without changes
|