144 lines
4.3 KiB
Python
144 lines
4.3 KiB
Python
|
|
"""MCP-Tool-Server für die Agent Platform."""
|
||
|
|
import os
|
||
|
|
import ast
|
||
|
|
import operator
|
||
|
|
import ipaddress
|
||
|
|
import socket
|
||
|
|
from datetime import datetime, timezone, timedelta
|
||
|
|
from urllib.parse import urlparse
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
from fastmcp import FastMCP
|
||
|
|
|
||
|
|
|
||
|
|
mcp = FastMCP("Agent Platform Tools")
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def echo(text: str) -> dict:
|
||
|
|
"""Echo-Tool zum Testen der MCP-Verbindung."""
|
||
|
|
return {"input": text, "timestamp": datetime.utcnow().isoformat()}
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def get_time(timezone_name: str = "UTC") -> dict:
|
||
|
|
"""Gibt aktuelle Zeit in angegebener Zeitzone zurück."""
|
||
|
|
tz_offsets = {"UTC": 0, "CET": 1, "CEST": 2, "EST": -5, "PST": -8, "JST": 9}
|
||
|
|
offset = tz_offsets.get(timezone_name.upper(), 0)
|
||
|
|
tz = timezone(timedelta(hours=offset))
|
||
|
|
return {"timezone": timezone_name, "time": datetime.now(tz).isoformat()}
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def calculate(expression: str) -> dict:
|
||
|
|
"""Berechnet einen mathematischen Ausdruck (sicher, nur Zahlen).
|
||
|
|
|
||
|
|
Erlaubt: +, -, *, /, **, %.
|
||
|
|
Beispiel: calculate("2 + 3 * 4") -> 14
|
||
|
|
"""
|
||
|
|
ops = {
|
||
|
|
ast.Add: operator.add,
|
||
|
|
ast.Sub: operator.sub,
|
||
|
|
ast.Mult: operator.mul,
|
||
|
|
ast.Div: operator.truediv,
|
||
|
|
ast.Pow: operator.pow,
|
||
|
|
ast.Mod: operator.mod,
|
||
|
|
}
|
||
|
|
|
||
|
|
try:
|
||
|
|
tree = ast.parse(expression, mode="eval")
|
||
|
|
except SyntaxError:
|
||
|
|
return {"error": "Ungültiger Ausdruck"}
|
||
|
|
|
||
|
|
def _eval(node):
|
||
|
|
if isinstance(node, ast.Constant):
|
||
|
|
if not isinstance(node.value, (int, float)):
|
||
|
|
raise ValueError("Nur Zahlen erlaubt")
|
||
|
|
return node.value
|
||
|
|
if isinstance(node, ast.BinOp):
|
||
|
|
if type(node.op) not in ops:
|
||
|
|
raise ValueError("Operation nicht erlaubt")
|
||
|
|
return ops[type(node.op)](_eval(node.left), _eval(node.right))
|
||
|
|
if isinstance(node, ast.UnaryOp):
|
||
|
|
if isinstance(node.op, ast.USub):
|
||
|
|
return -_eval(node.operand)
|
||
|
|
if isinstance(node.op, ast.UAdd):
|
||
|
|
return _eval(node.operand)
|
||
|
|
raise ValueError("Ungültiger Knoten")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = _eval(tree.body)
|
||
|
|
return {"expression": expression, "result": result}
|
||
|
|
except Exception as e:
|
||
|
|
return {"expression": expression, "error": str(e)}
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def http_get(url: str, timeout: int = 10) -> dict:
|
||
|
|
"""Holt eine URL via HTTP. SSRF-Schutz aktiv.
|
||
|
|
|
||
|
|
Nur http/https, keine Auth, keine privaten IPs.
|
||
|
|
Beispiel: http_get("https://example.com")
|
||
|
|
"""
|
||
|
|
if not url.startswith(("http://", "https://")):
|
||
|
|
return {"error": "Nur http/https URLs erlaubt"}
|
||
|
|
|
||
|
|
parsed = urlparse(url)
|
||
|
|
try:
|
||
|
|
ip = socket.gethostbyname(parsed.hostname)
|
||
|
|
ip_obj = ipaddress.ip_address(ip)
|
||
|
|
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved:
|
||
|
|
return {"error": f"IP {ip} blockiert (privat/loopback/reserved)"}
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
try:
|
||
|
|
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
|
||
|
|
r = client.get(url)
|
||
|
|
return {
|
||
|
|
"status": r.status_code,
|
||
|
|
"url": str(r.url),
|
||
|
|
"content_type": r.headers.get("content-type", ""),
|
||
|
|
"length": len(r.text),
|
||
|
|
"text_preview": r.text[:2000],
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
return {"error": str(e)}
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def list_env(prefix: str = "") -> dict:
|
||
|
|
"""Listet Umgebungsvariablen mit optionalem Prefix-Filter.
|
||
|
|
|
||
|
|
Sensitive Werte (KEY/SECRET/TOKEN/PASSWORD) werden maskiert.
|
||
|
|
"""
|
||
|
|
env = {}
|
||
|
|
for k, v in os.environ.items():
|
||
|
|
if prefix and not k.startswith(prefix.upper()):
|
||
|
|
continue
|
||
|
|
upper = k.upper()
|
||
|
|
if any(s in upper for s in ["KEY", "SECRET", "TOKEN", "PASSWORD"]):
|
||
|
|
env[k] = "***MASKED***"
|
||
|
|
else:
|
||
|
|
env[k] = v[:200]
|
||
|
|
return {"count": len(env), "vars": env}
|
||
|
|
|
||
|
|
|
||
|
|
@mcp.tool()
|
||
|
|
def json_format(data: str, indent: int = 2) -> dict:
|
||
|
|
"""Formatiert einen JSON-String lesbar.
|
||
|
|
|
||
|
|
Beispiel: json_format('{"a":1}')
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
parsed = json.loads(data)
|
||
|
|
return {"formatted": json.dumps(parsed, indent=indent, ensure_ascii=False)}
|
||
|
|
except Exception as e:
|
||
|
|
return {"error": str(e)}
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
host = os.getenv("MCP_HOST", "0.0.0.0")
|
||
|
|
port = int(os.getenv("MCP_PORT", "8501"))
|
||
|
|
mcp.run(transport="streamable-http", host=host, port=port)
|