initial commit
This commit is contained in:
12
services/mcp-filesystem/Dockerfile
Normal file
12
services/mcp-filesystem/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server.py .
|
||||
|
||||
EXPOSE 8010
|
||||
|
||||
CMD ["python", "/app/server.py"]
|
||||
1
services/mcp-filesystem/requirements.txt
Normal file
1
services/mcp-filesystem/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
mcp[cli]>=1.10.0
|
||||
87
services/mcp-filesystem/server.py
Normal file
87
services/mcp-filesystem/server.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("filesystem", host="0.0.0.0", port=8010)
|
||||
|
||||
|
||||
def _allowed_roots() -> list[Path]:
|
||||
roots_raw = os.getenv("MCP_FILESYSTEM_ROOTS", "/workspace").split(",")
|
||||
roots = [Path(root.strip()).resolve() for root in roots_raw if root.strip()]
|
||||
return roots or [Path("/workspace").resolve()]
|
||||
|
||||
|
||||
def _resolve(path: str) -> Path:
|
||||
candidate = Path(path)
|
||||
if not candidate.is_absolute():
|
||||
candidate = Path("/workspace") / candidate
|
||||
resolved = candidate.resolve()
|
||||
|
||||
for root in _allowed_roots():
|
||||
try:
|
||||
resolved.relative_to(root)
|
||||
return resolved
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
allowed = ", ".join(str(r) for r in _allowed_roots())
|
||||
raise ValueError(f"Path '{resolved}' is outside allowed roots: {allowed}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_roots() -> list[str]:
|
||||
"""List configured filesystem roots that this MCP server can access."""
|
||||
return [str(root) for root in _allowed_roots()]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_dir(path: str = ".") -> list[str]:
|
||||
"""List entries in a directory relative to an allowed root."""
|
||||
target = _resolve(path)
|
||||
if not target.exists() or not target.is_dir():
|
||||
raise ValueError(f"Not a directory: {target}")
|
||||
|
||||
items = []
|
||||
for entry in sorted(target.iterdir(), key=lambda p: p.name.lower()):
|
||||
suffix = "/" if entry.is_dir() else ""
|
||||
items.append(f"{entry.name}{suffix}")
|
||||
return items
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def read_text(path: str, max_chars: int = 50000) -> str:
|
||||
"""Read text from a file in an allowed root."""
|
||||
target = _resolve(path)
|
||||
if not target.exists() or not target.is_file():
|
||||
raise ValueError(f"Not a file: {target}")
|
||||
|
||||
content = target.read_text(encoding="utf-8")
|
||||
return content[:max_chars]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def write_text(path: str, content: str, append: bool = False) -> str:
|
||||
"""Write text to a file in an allowed root."""
|
||||
target = _resolve(path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mode = "a" if append else "w"
|
||||
with target.open(mode, encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
|
||||
return str(target)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def find_files(path: str = ".", pattern: str = "*") -> list[str]:
|
||||
"""Find files by glob pattern under a path in an allowed root."""
|
||||
target = _resolve(path)
|
||||
if not target.exists() or not target.is_dir():
|
||||
raise ValueError(f"Not a directory: {target}")
|
||||
|
||||
return [str(p) for p in target.rglob(pattern) if p.is_file()]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="sse")
|
||||
12
services/mcp-k8s/Dockerfile
Normal file
12
services/mcp-k8s/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server.py .
|
||||
|
||||
EXPOSE 8012
|
||||
|
||||
CMD ["python", "/app/server.py"]
|
||||
2
services/mcp-k8s/requirements.txt
Normal file
2
services/mcp-k8s/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mcp[cli]>=1.10.0
|
||||
kubernetes>=30.1.0
|
||||
78
services/mcp-k8s/server.py
Normal file
78
services/mcp-k8s/server.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("k8s-cluster", host="0.0.0.0", port=8012)
|
||||
|
||||
|
||||
def _load_kube() -> None:
|
||||
kubeconfig = os.getenv("KUBECONFIG")
|
||||
try:
|
||||
if kubeconfig and os.path.exists(kubeconfig):
|
||||
config.load_kube_config(config_file=kubeconfig)
|
||||
else:
|
||||
config.load_incluster_config()
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Failed to load Kubernetes config: {exc}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_namespaces() -> list[str]:
|
||||
"""List all Kubernetes namespaces visible to this server."""
|
||||
_load_kube()
|
||||
api = client.CoreV1Api()
|
||||
return [item.metadata.name for item in api.list_namespace().items]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_pods(namespace: str | None = None, limit: int = 100) -> list[dict]:
|
||||
"""List pods in a namespace with status and node placement."""
|
||||
_load_kube()
|
||||
api = client.CoreV1Api()
|
||||
ns = namespace or os.getenv("K8S_DEFAULT_NAMESPACE", "default")
|
||||
|
||||
pods = api.list_namespaced_pod(ns, limit=limit).items
|
||||
return [
|
||||
{
|
||||
"name": pod.metadata.name,
|
||||
"phase": pod.status.phase,
|
||||
"node": pod.spec.node_name,
|
||||
"created": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
|
||||
}
|
||||
for pod in pods
|
||||
]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_pod_logs(namespace: str, pod_name: str, tail_lines: int = 200) -> str:
|
||||
"""Get recent logs from a specific pod."""
|
||||
_load_kube()
|
||||
api = client.CoreV1Api()
|
||||
return api.read_namespaced_pod_log(name=pod_name, namespace=namespace, tail_lines=tail_lines)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_deployments(namespace: str | None = None) -> list[dict]:
|
||||
"""List deployments in the namespace with availability details."""
|
||||
_load_kube()
|
||||
api = client.AppsV1Api()
|
||||
ns = namespace or os.getenv("K8S_DEFAULT_NAMESPACE", "default")
|
||||
|
||||
try:
|
||||
deps = api.list_namespaced_deployment(ns).items
|
||||
except ApiException as exc:
|
||||
raise RuntimeError(f"Kubernetes API error: {exc}")
|
||||
|
||||
return [
|
||||
{
|
||||
"name": dep.metadata.name,
|
||||
"ready_replicas": dep.status.ready_replicas or 0,
|
||||
"replicas": dep.status.replicas or 0,
|
||||
}
|
||||
for dep in deps
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="sse")
|
||||
12
services/mcp-media/Dockerfile
Normal file
12
services/mcp-media/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server.py .
|
||||
|
||||
EXPOSE 8013
|
||||
|
||||
CMD ["python", "/app/server.py"]
|
||||
2
services/mcp-media/requirements.txt
Normal file
2
services/mcp-media/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mcp[cli]>=1.10.0
|
||||
requests>=2.32.3
|
||||
83
services/mcp-media/server.py
Normal file
83
services/mcp-media/server.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("media-stack", host="0.0.0.0", port=8013)
|
||||
|
||||
|
||||
def _get_json(url: str, headers: dict[str, str] | None = None, timeout: int = 15) -> Any:
|
||||
response = requests.get(url, headers=headers or {}, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def service_health() -> dict[str, str]:
|
||||
"""Check reachability of Radarr, Sonarr, qBittorrent, and Plex endpoints."""
|
||||
checks = {
|
||||
"radarr": (os.getenv("RADARR_URL", ""), "/api/v3/system/status", {"X-Api-Key": os.getenv("RADARR_API_KEY", "")}),
|
||||
"sonarr": (os.getenv("SONARR_URL", ""), "/api/v3/system/status", {"X-Api-Key": os.getenv("SONARR_API_KEY", "")}),
|
||||
"qbit": (os.getenv("QBIT_URL", ""), "/api/v2/app/version", {}),
|
||||
"plex": (os.getenv("PLEX_URL", ""), "/identity", {"X-Plex-Token": os.getenv("PLEX_TOKEN", "")}),
|
||||
}
|
||||
|
||||
status = {}
|
||||
for name, (base, path, headers) in checks.items():
|
||||
if not base:
|
||||
status[name] = "not_configured"
|
||||
continue
|
||||
try:
|
||||
requests.get(f"{base.rstrip('/')}{path}", headers=headers, timeout=10).raise_for_status()
|
||||
status[name] = "ok"
|
||||
except Exception as exc:
|
||||
status[name] = f"error: {exc}"
|
||||
|
||||
return status
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def radarr_queue(page: int = 1, page_size: int = 20) -> Any:
|
||||
"""Return current Radarr queue items."""
|
||||
base = os.getenv("RADARR_URL", "")
|
||||
key = os.getenv("RADARR_API_KEY", "")
|
||||
url = f"{base.rstrip('/')}/api/v3/queue?page={page}&pageSize={page_size}"
|
||||
return _get_json(url, headers={"X-Api-Key": key})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def sonarr_queue(page: int = 1, page_size: int = 20) -> Any:
|
||||
"""Return current Sonarr queue items."""
|
||||
base = os.getenv("SONARR_URL", "")
|
||||
key = os.getenv("SONARR_API_KEY", "")
|
||||
url = f"{base.rstrip('/')}/api/v3/queue?page={page}&pageSize={page_size}"
|
||||
return _get_json(url, headers={"X-Api-Key": key})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def qbit_torrents(filter_name: str = "all") -> Any:
|
||||
"""Return qBittorrent torrent list by filter (all/downloading/completed/etc)."""
|
||||
base = os.getenv("QBIT_URL", "")
|
||||
user = os.getenv("QBIT_USERNAME", "")
|
||||
password = os.getenv("QBIT_PASSWORD", "")
|
||||
|
||||
session = requests.Session()
|
||||
session.post(f"{base.rstrip('/')}/api/v2/auth/login", data={"username": user, "password": password}, timeout=15).raise_for_status()
|
||||
response = session.get(f"{base.rstrip('/')}/api/v2/torrents/info", params={"filter": filter_name}, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def plex_sessions() -> Any:
|
||||
"""Return active Plex sessions."""
|
||||
base = os.getenv("PLEX_URL", "")
|
||||
token = os.getenv("PLEX_TOKEN", "")
|
||||
headers = {"X-Plex-Token": token, "Accept": "application/json"}
|
||||
url = f"{base.rstrip('/')}/status/sessions"
|
||||
return _get_json(url, headers=headers)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="sse")
|
||||
Reference in New Issue
Block a user