MCP Server pre MinIO: Daj Claudeovi prístup k tvojim súborom
18 min
MCP Server pre MinIO: Daj Claudeovi prístup k tvojim súborom
"Claude, ukáž mi screenshoty z posledného test runu." 🤖
A Claude to spraví. Vďaka MCP.
Prolog: The Problem
javascript
// Before MCP:
you = "Claude, analyzuj ten screenshot z testu"
claude = "Nahraj mi screenshot..."
you = *downloads from MinIO*
you = *uploads to Claude*
claude = "OK, analyzujem..."
// 5 minút manuálnej práce 😰
// After MCP:
you = "Claude, analyzuj screenshot test-123/login.png"
claude = *fetches from MinIO directly*
claude = "Screenshot ukazuje error message: 'Invalid credentials'..."
// 5 sekúnd! ✅
// What happened?
// MCP = Model Context Protocol
// Claude má DIRECT access k MinIO! 🚀Real-world scenár:
Problém:
- Test automation generuje tisícky screenshotov v MinIO
- Chceš aby Claude analyzoval failure screenshoty
- Ale musíš každý screenshot manuálne stiahnuť a nahrať
S MCP:
- Claude má direct access k MinIO
- "Show me all screenshots from failed tests today"
- Claude listuje MinIO, analyzuje obrázky, vyhodnotí problémy
- Automaticky! 🎉
Time saved:
- Before: 30 minút na analýzu (download, upload, analyze)
- After: 30 sekúnd (Claude robí všetko)
- That's 60× faster! 🚀Poďme na to!
Kapitola 1: Čo je MCP?
Model Context Protocol Explained
yaml
MCP (Model Context Protocol):
what: "Protocol pre AI agentov k externým dátam"
created_by: "Anthropic"
purpose: "Daj AI prístup k tvojim súborom, DB, API..."
How it works:
┌─────────────┐
│ Claude │
│ (AI) │
└──────┬──────┘
│ MCP Protocol
│
┌──────▼──────────┐
│ MCP Server │
│ (Your code) │
└──────┬──────────┘
│
┌──────▼──────────┐
│ MinIO │
│ (Your files) │
└─────────────────┘
Claude: "Show me test-123 screenshots"
→ MCP Server: fetches from MinIO
→ Returns images to Claude
→ Claude analyzesBenefits:
✅ Direct access (no manual upload)
✅ Real-time data (always fresh)
✅ Secure (your infrastructure)
✅ Flexible (any data source)
✅ Powerful (AI + your data)Why MCP for MinIO?
javascript
use_cases = {
"Test Analysis": {
"Ask": "Analyze failed test screenshots",
"MCP": "Fetches screenshots, Claude analyzes",
"Result": "Root cause identified in 30s"
},
"Document Processing": {
"Ask": "Summarize all PDFs in bucket",
"MCP": "Lists PDFs, Claude reads & summarizes",
"Result": "All documents processed automatically"
},
"Log Analysis": {
"Ask": "Find errors in today's logs",
"MCP": "Fetches logs, Claude searches patterns",
"Result": "All errors found & categorized"
},
"Video Analysis": {
"Ask": "What happens in test video at 00:45?",
"MCP": "Fetches video frame, Claude describes",
"Result": "Detailed description of UI state"
}
}
verdict = "MCP + MinIO = AI Superpowers! 🚀"Kapitola 2: Prerequisites
What You Need
bash
# 1. MinIO running (from previous article!)
docker ps | grep minio
# Should see: minio container running on 9000/9001
# 2. Python 3.10+ (for MCP server)
python --version
# Output: Python 3.10+
# 3. Node.js 18+ (for testing)
node --version
# Output: v18+
# 4. Claude Desktop App (for usage)
# Download: https://claude.ai/downloadProject Structure
minio-mcp-server/
├── src/
│ ├── __init__.py
│ ├── server.py # MCP server implementation
│ └── minio_client.py # MinIO operations
├── pyproject.toml # Dependencies
├── README.md
└── .env # ConfigurationKapitola 3: Implementation
Step 1: Setup Project
bash
# Create project
mkdir minio-mcp-server
cd minio-mcp-server
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Create pyproject.toml
cat > pyproject.toml << 'EOF'
[project]
name = "minio-mcp-server"
version = "0.1.0"
description = "MCP server for MinIO object storage"
requires-python = ">=3.10"
dependencies = [
"mcp>=0.9.0",
"minio>=7.2.0",
"python-dotenv>=1.0.0"
]
[project.scripts]
minio-mcp = "minio_mcp_server:main"
EOF
# Install dependencies
pip install -e .Step 2: Configuration
bash
# Create .env file
cat > .env << 'EOF'
# MinIO Configuration
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=password123
MINIO_SECURE=false
# MCP Configuration
MCP_SERVER_NAME=minio-server
EOFStep 3: MinIO Client
python
# src/minio_client.py
from minio import Minio
from minio.error import S3Error
from typing import List, Dict, Optional
import io
import os
from datetime import timedelta
class MinioClient:
"""MinIO client for MCP server"""
def __init__(self):
self.client = Minio(
endpoint=os.getenv('MINIO_ENDPOINT', 'localhost:9000'),
access_key=os.getenv('MINIO_ACCESS_KEY', 'admin'),
secret_key=os.getenv('MINIO_SECRET_KEY', 'password123'),
secure=os.getenv('MINIO_SECURE', 'false').lower() == 'true'
)
def list_buckets(self) -> List[Dict[str, str]]:
"""List all buckets"""
try:
buckets = self.client.list_buckets()
return [
{
'name': bucket.name,
'creation_date': bucket.creation_date.isoformat()
}
for bucket in buckets
]
except S3Error as e:
raise Exception(f"Failed to list buckets: {e}")
def list_objects(self, bucket: str, prefix: str = '', recursive: bool = True) -> List[Dict[str, any]]:
"""List objects in bucket"""
try:
objects = self.client.list_objects(
bucket_name=bucket,
prefix=prefix,
recursive=recursive
)
return [
{
'name': obj.object_name,
'size': obj.size,
'last_modified': obj.last_modified.isoformat(),
'etag': obj.etag,
'content_type': obj.content_type
}
for obj in objects
]
except S3Error as e:
raise Exception(f"Failed to list objects: {e}")
def get_object(self, bucket: str, object_name: str) -> bytes:
"""Get object content"""
try:
response = self.client.get_object(bucket, object_name)
data = response.read()
response.close()
response.release_conn()
return data
except S3Error as e:
raise Exception(f"Failed to get object: {e}")
def get_object_metadata(self, bucket: str, object_name: str) -> Dict[str, any]:
"""Get object metadata"""
try:
stat = self.client.stat_object(bucket, object_name)
return {
'name': object_name,
'size': stat.size,
'last_modified': stat.last_modified.isoformat(),
'etag': stat.etag,
'content_type': stat.content_type,
'metadata': stat.metadata
}
except S3Error as e:
raise Exception(f"Failed to get metadata: {e}")
def put_object(self, bucket: str, object_name: str, data: bytes, content_type: str = 'application/octet-stream') -> Dict[str, str]:
"""Upload object"""
try:
result = self.client.put_object(
bucket_name=bucket,
object_name=object_name,
data=io.BytesIO(data),
length=len(data),
content_type=content_type
)
return {
'bucket': bucket,
'object_name': object_name,
'etag': result.etag,
'version_id': result.version_id
}
except S3Error as e:
raise Exception(f"Failed to upload object: {e}")
def delete_object(self, bucket: str, object_name: str) -> Dict[str, str]:
"""Delete object"""
try:
self.client.remove_object(bucket, object_name)
return {
'bucket': bucket,
'object_name': object_name,
'status': 'deleted'
}
except S3Error as e:
raise Exception(f"Failed to delete object: {e}")
def get_presigned_url(self, bucket: str, object_name: str, expires: int = 3600) -> str:
"""Get presigned URL"""
try:
url = self.client.presigned_get_object(
bucket_name=bucket,
object_name=object_name,
expires=timedelta(seconds=expires)
)
return url
except S3Error as e:
raise Exception(f"Failed to generate URL: {e}")
def search_objects(self, bucket: str, pattern: str) -> List[Dict[str, any]]:
"""Search objects by pattern"""
try:
all_objects = self.list_objects(bucket, recursive=True)
# Simple pattern matching (contains)
matching = [
obj for obj in all_objects
if pattern.lower() in obj['name'].lower()
]
return matching
except Exception as e:
raise Exception(f"Failed to search objects: {e}")Step 4: MCP Server
python
# src/server.py
import asyncio
import os
from typing import Any
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
from dotenv import load_dotenv
from .minio_client import MinioClient
# Load environment variables
load_dotenv()
# Create MCP server
app = Server("minio-server")
# Create MinIO client
minio_client = MinioClient()
@app.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""List available tools"""
return [
types.Tool(
name="list_buckets",
description="List all MinIO buckets",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
types.Tool(
name="list_objects",
description="List objects in a bucket",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"prefix": {
"type": "string",
"description": "Object prefix filter (optional)",
"default": ""
}
},
"required": ["bucket"]
}
),
types.Tool(
name="get_object",
description="Get object content from MinIO",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"object_name": {
"type": "string",
"description": "Object name/path"
}
},
"required": ["bucket", "object_name"]
}
),
types.Tool(
name="get_object_metadata",
description="Get object metadata",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"object_name": {
"type": "string",
"description": "Object name/path"
}
},
"required": ["bucket", "object_name"]
}
),
types.Tool(
name="search_objects",
description="Search objects by name pattern",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"pattern": {
"type": "string",
"description": "Search pattern (substring match)"
}
},
"required": ["bucket", "pattern"]
}
),
types.Tool(
name="upload_object",
description="Upload object to MinIO",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"object_name": {
"type": "string",
"description": "Object name/path"
},
"content": {
"type": "string",
"description": "Content to upload (base64 for binary)"
},
"content_type": {
"type": "string",
"description": "Content type",
"default": "application/octet-stream"
}
},
"required": ["bucket", "object_name", "content"]
}
),
types.Tool(
name="delete_object",
description="Delete object from MinIO",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"object_name": {
"type": "string",
"description": "Object name/path"
}
},
"required": ["bucket", "object_name"]
}
),
types.Tool(
name="get_presigned_url",
description="Generate presigned URL for object",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": "Bucket name"
},
"object_name": {
"type": "string",
"description": "Object name/path"
},
"expires": {
"type": "integer",
"description": "URL expiry in seconds",
"default": 3600
}
},
"required": ["bucket", "object_name"]
}
)
]
@app.call_tool()
async def handle_call_tool(
name: str,
arguments: dict[str, Any]
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool calls"""
try:
if name == "list_buckets":
buckets = minio_client.list_buckets()
return [
types.TextContent(
type="text",
text=f"Found {len(buckets)} buckets:\n" +
"\n".join([f"- {b['name']}" for b in buckets])
)
]
elif name == "list_objects":
bucket = arguments["bucket"]
prefix = arguments.get("prefix", "")
objects = minio_client.list_objects(bucket, prefix)
if not objects:
return [types.TextContent(type="text", text=f"No objects found in bucket '{bucket}'")]
text = f"Found {len(objects)} objects in '{bucket}':\n"
for obj in objects[:50]: # Limit to 50 for readability
text += f"\n- {obj['name']} ({obj['size']} bytes)"
if len(objects) > 50:
text += f"\n\n... and {len(objects) - 50} more objects"
return [types.TextContent(type="text", text=text)]
elif name == "get_object":
bucket = arguments["bucket"]
object_name = arguments["object_name"]
# Get metadata first
metadata = minio_client.get_object_metadata(bucket, object_name)
content_type = metadata.get('content_type', 'application/octet-stream')
# Get content
data = minio_client.get_object(bucket, object_name)
# Handle images
if content_type.startswith('image/'):
import base64
return [
types.ImageContent(
type="image",
data=base64.b64encode(data).decode('utf-8'),
mimeType=content_type
)
]
# Handle text
elif content_type.startswith('text/') or 'json' in content_type:
try:
text = data.decode('utf-8')
return [types.TextContent(type="text", text=text)]
except:
return [types.TextContent(type="text", text="[Binary content - cannot display as text]")]
# Handle other files
else:
return [
types.TextContent(
type="text",
text=f"Object: {object_name}\nType: {content_type}\nSize: {len(data)} bytes\n\n[Binary content]"
)
]
elif name == "get_object_metadata":
bucket = arguments["bucket"]
object_name = arguments["object_name"]
metadata = minio_client.get_object_metadata(bucket, object_name)
text = f"Metadata for '{object_name}':\n"
text += f"- Size: {metadata['size']} bytes\n"
text += f"- Last Modified: {metadata['last_modified']}\n"
text += f"- Content Type: {metadata['content_type']}\n"
text += f"- ETag: {metadata['etag']}\n"
return [types.TextContent(type="text", text=text)]
elif name == "search_objects":
bucket = arguments["bucket"]
pattern = arguments["pattern"]
objects = minio_client.search_objects(bucket, pattern)
if not objects:
return [types.TextContent(type="text", text=f"No objects matching '{pattern}' found")]
text = f"Found {len(objects)} objects matching '{pattern}':\n"
for obj in objects[:20]: # Limit to 20
text += f"\n- {obj['name']} ({obj['size']} bytes)"
if len(objects) > 20:
text += f"\n\n... and {len(objects) - 20} more matches"
return [types.TextContent(type="text", text=text)]
elif name == "upload_object":
bucket = arguments["bucket"]
object_name = arguments["object_name"]
content = arguments["content"]
content_type = arguments.get("content_type", "application/octet-stream")
# Convert content to bytes
import base64
try:
data = base64.b64decode(content)
except:
data = content.encode('utf-8')
result = minio_client.put_object(bucket, object_name, data, content_type)
return [
types.TextContent(
type="text",
text=f"Uploaded '{object_name}' to bucket '{bucket}' successfully"
)
]
elif name == "delete_object":
bucket = arguments["bucket"]
object_name = arguments["object_name"]
result = minio_client.delete_object(bucket, object_name)
return [
types.TextContent(
type="text",
text=f"Deleted '{object_name}' from bucket '{bucket}' successfully"
)
]
elif name == "get_presigned_url":
bucket = arguments["bucket"]
object_name = arguments["object_name"]
expires = arguments.get("expires", 3600)
url = minio_client.get_presigned_url(bucket, object_name, expires)
return [
types.TextContent(
type="text",
text=f"Presigned URL (expires in {expires}s):\n{url}"
)
]
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
return [
types.TextContent(
type="text",
text=f"Error: {str(e)}"
)
]
async def main():
"""Run MCP server"""
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="minio-server",
server_version="0.1.0",
capabilities=app.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())Step 5: Entry Point
python
# src/__init__.py
from .server import main
__all__ = ['main']Kapitola 4: Testing MCP Server
Test Locally
bash
# Run MCP server (stdio mode)
python -m minio_mcp_server
# Server is running, waiting for input...
# Test with MCP Inspector (optional)
npx @modelcontextprotocol/inspector python -m minio_mcp_server
# Open browser: http://localhost:5173
# You can test all tools interactively!Manual Testing
bash
# Install mcp CLI
npm install -g @modelcontextprotocol/cli
# Test list_buckets
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_buckets","arguments":{}}}' | \
python -m minio_mcp_server
# Test list_objects
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_objects","arguments":{"bucket":"test-screenshots"}}}' | \
python -m minio_mcp_serverKapitola 5: Integration with Claude Desktop
Configure Claude Desktop
json
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
// Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"minio": {
"command": "python",
"args": [
"-m",
"minio_mcp_server"
],
"env": {
"MINIO_ENDPOINT": "localhost:9000",
"MINIO_ACCESS_KEY": "admin",
"MINIO_SECRET_KEY": "password123",
"MINIO_SECURE": "false"
}
}
}
}Alternative: Use full path
json
{
"mcpServers": {
"minio": {
"command": "/path/to/venv/bin/python",
"args": [
"-m",
"minio_mcp_server"
],
"env": {
"MINIO_ENDPOINT": "localhost:9000",
"MINIO_ACCESS_KEY": "admin",
"MINIO_SECRET_KEY": "password123"
}
}
}
}Restart Claude Desktop
bash
# macOS
killall Claude
open -a Claude
# Windows
# Close Claude from taskbar
# Open Claude againVerify Integration
Open Claude Desktop
→ Look for 🔌 icon (MCP connected)
→ Click 🔌 to see available servers
→ Should see "minio" server listed ✅Kapitola 6: Usage Examples
Example 1: List Buckets
You: "What buckets do I have in MinIO?"
Claude: [calls list_buckets tool]
Claude: "You have 3 buckets in MinIO:
- test-screenshots
- test-videos
- documents"Example 2: Analyze Screenshot
You: "Show me the latest screenshot from test-screenshots bucket"
Claude: [calls list_objects with bucket="test-screenshots"]
Claude: [calls get_object with latest screenshot]
Claude: [analyzes image]
Claude: "The latest screenshot shows a login page with
an error message: 'Invalid username or password'.
The error is displayed in red text below the login form."Example 3: Search Files
You: "Find all files with 'error' in the name from test-screenshots"
Claude: [calls search_objects with pattern="error"]
Claude: "Found 15 files with 'error' in the name:
- test-123/error-login.png
- test-124/error-checkout.png
- test-125/error-payment.png
..."Example 4: Compare Screenshots
You: "Compare screenshots test-123/before.png and test-123/after.png"
Claude: [calls get_object for both images]
Claude: [analyzes both images]
Claude: "Comparing the two screenshots:
Before:
- Button is blue
- Text says 'Submit'
- No loading indicator
After:
- Button is gray
- Text says 'Processing...'
- Loading spinner visible
The after screenshot shows the button in a disabled/loading state."Example 5: Log Analysis
You: "Analyze all error logs from today"
Claude: [calls list_objects with prefix="logs/2024-12-23"]
Claude: [calls get_object for each log file]
Claude: [analyzes content]
Claude: "Found 47 error logs from today. Summary:
Top errors:
1. Connection timeout (15 occurrences)
2. Invalid token (12 occurrences)
3. Database deadlock (8 occurrences)
Most problematic service: payment-service (23 errors)
Recommendation: Check database connection pool and
investigate token validation logic."Example 6: Document Summary
You: "Summarize all PDFs in the documents bucket"
Claude: [calls list_objects with bucket="documents"]
Claude: [filters .pdf files]
Claude: [calls get_object for each PDF]
Claude: [extracts and summarizes text]
Claude: "Found 12 PDF documents:
1. Q4-2024-Report.pdf (15 pages)
Summary: Financial results showing 23% growth...
2. Product-Roadmap-2025.pdf (8 pages)
Summary: Plans for 5 major features...
3. Security-Audit.pdf (42 pages)
Summary: Comprehensive audit identifying 3 high-priority issues...
..."Kapitola 7: Advanced Features
Add Resource Support
python
# src/server.py (add this)
@app.list_resources()
async def handle_list_resources() -> list[types.Resource]:
"""List available resources"""
buckets = minio_client.list_buckets()
resources = []
for bucket in buckets:
# Add bucket as resource
resources.append(
types.Resource(
uri=f"minio://{bucket['name']}/",
name=f"Bucket: {bucket['name']}",
description=f"MinIO bucket '{bucket['name']}'",
mimeType="application/x-directory"
)
)
return resources
@app.read_resource()
async def handle_read_resource(uri: str) -> str:
"""Read resource content"""
# Parse URI: minio://bucket/path
if not uri.startswith("minio://"):
raise ValueError("Invalid URI scheme")
path = uri.replace("minio://", "")
parts = path.split("/", 1)
bucket = parts[0]
object_name = parts[1] if len(parts) > 1 else ""
if not object_name:
# List bucket contents
objects = minio_client.list_objects(bucket)
return "\n".join([obj['name'] for obj in objects])
else:
# Get object content
data = minio_client.get_object(bucket, object_name)
try:
return data.decode('utf-8')
except:
return "[Binary content]"Add Prompts
python
# src/server.py (add this)
@app.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
"""List available prompts"""
return [
types.Prompt(
name="analyze_test_failures",
description="Analyze failed test screenshots",
arguments=[
types.PromptArgument(
name="bucket",
description="Bucket name",
required=True
),
types.PromptArgument(
name="test_id",
description="Test ID",
required=True
)
]
),
types.Prompt(
name="summarize_logs",
description="Summarize log files",
arguments=[
types.PromptArgument(
name="bucket",
description="Bucket name",
required=True
),
types.PromptArgument(
name="date",
description="Date (YYYY-MM-DD)",
required=True
)
]
)
]
@app.get_prompt()
async def handle_get_prompt(
name: str,
arguments: dict[str, str]
) -> types.GetPromptResult:
"""Get prompt content"""
if name == "analyze_test_failures":
bucket = arguments["bucket"]
test_id = arguments["test_id"]
return types.GetPromptResult(
description=f"Analyze test failures for {test_id}",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"""Please analyze the test failure for test ID: {test_id}
1. List all screenshots from bucket '{bucket}' with prefix '{test_id}/'
2. Examine each screenshot
3. Identify the failure point
4. Suggest root cause
5. Recommend fixes
Be thorough and specific."""
)
)
]
)
elif name == "summarize_logs":
bucket = arguments["bucket"]
date = arguments["date"]
return types.GetPromptResult(
description=f"Summarize logs for {date}",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"""Please analyze logs from {date}
1. List all log files from bucket '{bucket}' with prefix 'logs/{date}/'
2. Read and analyze each log file
3. Categorize errors by type
4. Count occurrences
5. Identify patterns
6. Recommend actions
Provide a clear summary with priorities."""
)
)
]
)
raise ValueError(f"Unknown prompt: {name}")Kapitola 8: Production Considerations
Security
python
# src/server.py (add validation)
def validate_bucket_access(bucket: str) -> bool:
"""Validate bucket access"""
allowed_buckets = os.getenv('ALLOWED_BUCKETS', '').split(',')
if allowed_buckets and bucket not in allowed_buckets:
raise PermissionError(f"Access to bucket '{bucket}' denied")
return True
# Use in tools:
@app.call_tool()
async def handle_call_tool(name: str, arguments: dict) -> list:
if 'bucket' in arguments:
validate_bucket_access(arguments['bucket'])
# ... rest of tool logicLogging
python
# src/server.py (add logging)
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('minio-mcp.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@app.call_tool()
async def handle_call_tool(name: str, arguments: dict) -> list:
logger.info(f"Tool called: {name} with args: {arguments}")
try:
result = # ... tool logic
logger.info(f"Tool {name} succeeded")
return result
except Exception as e:
logger.error(f"Tool {name} failed: {e}")
raiseError Handling
python
# src/minio_client.py (improve error handling)
class MinioError(Exception):
"""Base MinIO error"""
pass
class BucketNotFoundError(MinioError):
"""Bucket not found"""
pass
class ObjectNotFoundError(MinioError):
"""Object not found"""
pass
def get_object(self, bucket: str, object_name: str) -> bytes:
"""Get object with better error handling"""
try:
response = self.client.get_object(bucket, object_name)
data = response.read()
response.close()
response.release_conn()
return data
except S3Error as e:
if e.code == 'NoSuchBucket':
raise BucketNotFoundError(f"Bucket '{bucket}' not found")
elif e.code == 'NoSuchKey':
raise ObjectNotFoundError(f"Object '{object_name}' not found")
else:
raise MinioError(f"MinIO error: {e}")Performance
python
# src/minio_client.py (add caching)
from functools import lru_cache
from datetime import datetime, timedelta
class MinioClient:
def __init__(self):
self.cache = {}
self.cache_ttl = 60 # seconds
def list_objects_cached(self, bucket: str, prefix: str = '') -> List[Dict]:
"""List objects with caching"""
cache_key = f"{bucket}:{prefix}"
# Check cache
if cache_key in self.cache:
cached_data, cached_time = self.cache[cache_key]
if datetime.now() - cached_time < timedelta(seconds=self.cache_ttl):
return cached_data
# Fetch fresh data
data = self.list_objects(bucket, prefix)
self.cache[cache_key] = (data, datetime.now())
return dataKapitola 9: Deployment
Docker Container
dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml .
RUN pip install -e .
# Copy source
COPY src/ src/
# Set environment
ENV PYTHONUNBUFFERED=1
# Run server
CMD ["python", "-m", "minio_mcp_server"]yaml
# docker-compose.yml
version: '3.8'
services:
minio-mcp:
build: .
container_name: minio-mcp-server
environment:
MINIO_ENDPOINT: minio:9000
MINIO_ACCESS_KEY: admin
MINIO_SECRET_KEY: password123
MINIO_SECURE: "false"
depends_on:
- minio
networks:
- minio-network
minio:
image: minio/minio:latest
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: password123
command: server /data --console-address ":9001"
networks:
- minio-network
networks:
minio-network:
driver: bridgesystemd Service
ini
# /etc/systemd/system/minio-mcp.service
[Unit]
Description=MinIO MCP Server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/minio-mcp-server
Environment="MINIO_ENDPOINT=localhost:9000"
Environment="MINIO_ACCESS_KEY=admin"
Environment="MINIO_SECRET_KEY=password123"
ExecStart=/opt/minio-mcp-server/venv/bin/python -m minio_mcp_server
Restart=always
[Install]
WantedBy=multi-user.targetbash
# Enable and start service
sudo systemctl enable minio-mcp
sudo systemctl start minio-mcp
# Check status
sudo systemctl status minio-mcp
# View logs
sudo journalctl -u minio-mcp -fZáver
What We Built:
yaml
MCP Server for MinIO:
tools: 8 (list, get, search, upload, delete, etc.)
resources: MinIO buckets and objects
prompts: Analysis templates
Features:
✅ Direct Claude ↔ MinIO integration
✅ Image analysis (screenshots)
✅ Text processing (logs, documents)
✅ Search and discovery
✅ Automated workflows
Benefits:
✅ No manual file transfers
✅ Real-time data access
✅ Powerful AI analysis
✅ Automated troubleshooting
✅ Secure (your infrastructure)Use Cases Unlocked:
Test Automation:
- "Analyze all failed test screenshots"
- "What caused test-123 to fail?"
- "Compare before/after screenshots"
Log Analysis:
- "Find errors in today's logs"
- "Which service has most issues?"
- "Summarize error patterns"
Document Processing:
- "Summarize all PDFs"
- "Extract data from invoices"
- "Find contracts mentioning X"
Video Analysis:
- "What happens at 00:45 in test video?"
- "Describe UI changes in recording"
- "Find frame where error appears"Time Savings:
Before MCP:
- Download file from MinIO: 30s
- Upload to Claude: 30s
- Wait for analysis: 60s
- Repeat for each file: ×10 = 20 minutes
After MCP:
- "Claude, analyze all files": 30s
- Done! 🎉
That's 40× faster! 🚀Next Steps:
bash
# 1. Add more tools
- Batch operations
- File conversion
- Metadata tagging
# 2. Integrate with other services
- Database queries
- API calls
- Slack notifications
# 3. Create workflows
- Automated test analysis
- Daily log summaries
- Report generationMCP + MinIO = AI prístup k tvojim súborom. No manual work. No context switching. Just ask Claude. 🤖
P.S.: MCP je budúcnosť AI integration. This is just the beginning. 🚀
P.P.S.: Môžeš vytvoriť MCP server pre čokoľvek: databázy, API, filesystémy... Sky is the limit! ☁️
P.P.P.S.: Claude Code už podporuje MCP out of the box. Try it! 💪