Files
office_translator/mcp_server.py
sepehr 67365918ae
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m36s
feat(ui,api): wave 3 — editorial pricing, real cancel, server history, DeepL purge
Pricing: full editorial redesign — serif card headers with accent pills
replace the colored font-black blocks, tone sweep across toggle/metrics/
features/CTAs, PLAN_COLORS removed; one design system app-wide.

Translate: decorative titles one step down (CTA hierarchy restored);
glossary and image-translation blocks hidden entirely for free users
(progressive disclosure — three controls for free).

Reviews: XLIFF hint line explains the exchange format; backend errors
routed through a friendly mapper (session/not-found/rate-limit/server).

Landing: fabricated hero UI cards (fake 'Context Engine' overlay)
removed — the photo no longer promises screens that don't exist.

Nav: single DashboardNavLinks component shared by sidebar and mobile
drawer (was duplicated markup).

API: GET /api/v1/translations (user job history, paginated; completed
jobs retained 24h) and POST /api/v1/translations/{id}/cancel —
cooperative cancellation with worker checkpoints before dispatch and
before finalisation, reserved quota released immediately. Translate
monitor now offers a real 'Cancel translation' next to 'Back to start';
recent-jobs list reads server history first, localStorage fallback.

DeepL purge (backend): provider module, registry registration, config
attrs/defaults, dispatch branch, admin settings schema + test branch,
legacy availability block, validation rules, plan provider lists,
error-code mappings, MCP enums, translator prompt mention, related
tests updated/removed. Fallback resolver skips unknown providers, so
stale chains containing 'deepl' degrade gracefully.

Verified: backend 110 tests passed; frontend build exit 0, vitest 9/9,
0 missing i18n keys, eslint 63 errors (vs 64 at HEAD).
2026-08-30 22:42:29 +02:00

329 lines
12 KiB
Python

#!/usr/bin/env python3
"""
MCP Server for Document Translation API
Model Context Protocol server for AI assistant integration
"""
import sys
import json
import asyncio
import base64
import requests
from pathlib import Path
from typing import Any, Optional
# MCP Protocol Constants
JSONRPC_VERSION = "2.0"
class MCPServer:
"""MCP Server for Document Translation"""
def __init__(self):
self.api_base = "http://localhost:8000"
self.capabilities = {
"tools": {}
}
def get_tools(self) -> list:
"""Return list of available tools"""
return [
{
"name": "translate_document",
"description": "Translate a document (Excel, Word, PowerPoint) to another language while preserving formatting",
"inputSchema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the document file (.xlsx, .docx, .pptx)"
},
"target_language": {
"type": "string",
"description": "Target language code (e.g., 'en', 'fr', 'es', 'fa', 'de')"
},
"provider": {
"type": "string",
"enum": ["google", "google_cloud", "openai", "openrouter", "deepseek", "minimax", "zai"],
"description": "Translation provider (default: google)"
},
"translate_images": {
"type": "boolean",
"description": "Extract and translate text from images using vision model"
},
"system_prompt": {
"type": "string",
"description": "Custom system prompt with context, glossary, or instructions for LLM translation"
},
"output_path": {
"type": "string",
"description": "Path where to save the translated document (optional)"
}
},
"required": ["file_path", "target_language"]
}
},
{
"name": "get_supported_languages",
"description": "Get list of supported language codes for translation",
"inputSchema": {
"type": "object",
"properties": {}
}
},
{
"name": "configure_translation",
"description": "Configure translation settings",
"inputSchema": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": ["google", "google_cloud", "openai", "openrouter", "deepseek", "minimax", "zai"],
"description": "Default translation provider"
}
}
}
},
{
"name": "check_api_health",
"description": "Check if the translation API is running and healthy",
"inputSchema": {
"type": "object",
"properties": {}
}
}
]
async def handle_tool_call(self, name: str, arguments: dict) -> dict:
"""Handle tool calls"""
try:
if name == "translate_document":
return await self.translate_document(arguments)
elif name == "get_supported_languages":
return await self.get_supported_languages()
elif name == "configure_translation":
return await self.configure_translation(arguments)
elif name == "check_api_health":
return await self.check_api_health()
else:
return {"error": f"Unknown tool: {name}"}
except Exception as e:
return {"error": str(e)}
async def translate_document(self, args: dict) -> dict:
"""Translate a document file"""
file_path = Path(args["file_path"])
if not file_path.exists():
return {"error": f"File not found: {file_path}"}
# Prepare form data
with open(file_path, 'rb') as f:
files = {'file': (file_path.name, f)}
data = {
'target_language': args['target_language'],
'provider': args.get('provider', 'google'),
'translate_images': str(args.get('translate_images', False)).lower(),
}
if args.get('system_prompt'):
data['system_prompt'] = args['system_prompt']
try:
response = requests.post(
f"{self.api_base}/translate",
files=files,
data=data,
timeout=300 # 5 minutes timeout
)
if response.status_code == 200:
# Save translated file
output_path = args.get('output_path')
if not output_path:
output_path = file_path.parent / f"translated_{file_path.name}"
output_path = Path(output_path)
with open(output_path, 'wb') as out:
out.write(response.content)
return {
"success": True,
"message": f"Document translated successfully",
"output_path": str(output_path),
"source_file": str(file_path),
"target_language": args['target_language'],
"provider": args.get('provider', 'google')
}
else:
error_detail = response.json() if response.headers.get('content-type') == 'application/json' else response.text
return {"error": f"Translation failed: {error_detail}"}
except requests.exceptions.ConnectionError:
return {"error": "Cannot connect to translation API. Make sure the server is running on http://localhost:8000"}
except requests.exceptions.Timeout:
return {"error": "Translation request timed out"}
async def get_supported_languages(self) -> dict:
"""Get supported language codes"""
return {
"languages": [
{"code": "en", "name": "English"},
{"code": "fa", "name": "Persian/Farsi"},
{"code": "fr", "name": "French"},
{"code": "es", "name": "Spanish"},
{"code": "de", "name": "German"},
{"code": "it", "name": "Italian"},
{"code": "pt", "name": "Portuguese"},
{"code": "ru", "name": "Russian"},
{"code": "zh", "name": "Chinese"},
{"code": "ja", "name": "Japanese"},
{"code": "ko", "name": "Korean"},
{"code": "ar", "name": "Arabic"},
{"code": "nl", "name": "Dutch"},
{"code": "pl", "name": "Polish"},
{"code": "tr", "name": "Turkish"},
{"code": "vi", "name": "Vietnamese"},
{"code": "th", "name": "Thai"},
{"code": "hi", "name": "Hindi"},
{"code": "he", "name": "Hebrew"},
{"code": "sv", "name": "Swedish"}
]
}
async def configure_translation(self, args: dict) -> dict:
"""Configure translation settings"""
config = {}
config['provider'] = args.get('provider', 'google')
return {
"success": True,
"configuration": config
}
async def check_api_health(self) -> dict:
"""Check API health status"""
try:
response = requests.get(f"{self.api_base}/health", timeout=5)
if response.status_code == 200:
return {
"status": "healthy",
"api_url": self.api_base,
"details": response.json()
}
else:
return {"status": "unhealthy", "error": "API returned non-200 status"}
except requests.exceptions.ConnectionError:
return {
"status": "unavailable",
"error": "Cannot connect to API. Start the server with: python main.py"
}
def create_response(self, id: Any, result: Any) -> dict:
"""Create JSON-RPC response"""
return {
"jsonrpc": JSONRPC_VERSION,
"id": id,
"result": result
}
def create_error(self, id: Any, code: int, message: str) -> dict:
"""Create JSON-RPC error response"""
return {
"jsonrpc": JSONRPC_VERSION,
"id": id,
"error": {
"code": code,
"message": message
}
}
async def handle_message(self, message: dict) -> Optional[dict]:
"""Handle incoming JSON-RPC message"""
msg_id = message.get("id")
method = message.get("method")
params = message.get("params", {})
if method == "initialize":
return self.create_response(msg_id, {
"protocolVersion": "2024-11-05",
"capabilities": self.capabilities,
"serverInfo": {
"name": "document-translator",
"version": "1.0.0"
}
})
elif method == "notifications/initialized":
return None # No response needed for notifications
elif method == "tools/list":
return self.create_response(msg_id, {
"tools": self.get_tools()
})
elif method == "tools/call":
tool_name = params.get("name")
tool_args = params.get("arguments", {})
result = await self.handle_tool_call(tool_name, tool_args)
return self.create_response(msg_id, {
"content": [
{
"type": "text",
"text": json.dumps(result, indent=2, ensure_ascii=False)
}
]
})
elif method == "ping":
return self.create_response(msg_id, {})
else:
return self.create_error(msg_id, -32601, f"Method not found: {method}")
async def run(self):
"""Run the MCP server using stdio"""
while True:
try:
line = sys.stdin.readline()
if not line:
break
message = json.loads(line)
response = await self.handle_message(message)
if response:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
except json.JSONDecodeError as e:
error = self.create_error(None, -32700, f"Parse error: {e}")
sys.stdout.write(json.dumps(error) + "\n")
sys.stdout.flush()
except Exception as e:
error = self.create_error(None, -32603, f"Internal error: {e}")
sys.stdout.write(json.dumps(error) + "\n")
sys.stdout.flush()
def main():
"""Main entry point"""
server = MCPServer()
asyncio.run(server.run())
if __name__ == "__main__":
main()