Init commit

This commit is contained in:
pporcheret
2026-10-24 17:55:05 +02:00
parent 7152b10695
commit c9c3d5f223
122 changed files with 7154 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
COMFYUI_URL=http://192.168.1.13:8188
COMFYUI_TIMEOUT=300
WORKFLOWS_DIR=/opt/hermes/MCP_comfyui/workflows
COMFY_PROMPT_NODE=6
COMFY_VISION_IMAGE_NODE=1
COMFY_VISION_PROMPT_NODE=2
ALFRED_RENDER_URL=http://127.0.0.1:54321/api/render
+6
View File
@@ -0,0 +1,6 @@
COMFYUI_URL=http://192.168.1.13:8188
COMFYUI_TIMEOUT=300
WORKFLOWS_DIR=/opt/hermes/MCP_comfyui/workflows
COMFY_PROMPT_NODE=6
COMFY_VISION_IMAGE_NODE=1
COMFY_VISION_PROMPT_NODE=2
+3
View File
@@ -0,0 +1,3 @@
# MCP_comfyui
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
+151
View File
@@ -0,0 +1,151 @@
"""MCP ComfyUI pour Hermes avec transmission d'images vers l'interface Alfred.
Fonctionne avec un workflow API ComfyUI possédant un nœud de sauvegarde d'image.
L'envoi HTTP à Alfred est indépendant de la réponse textuelle d'Hermes.
"""
import json
import os
import time
import uuid
from pathlib import Path
from urllib.parse import urlencode
import requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv(Path(__file__).with_name(".env"))
mcp = FastMCP("Alfred ComfyUI")
BASE = os.getenv("COMFYUI_URL", "http://127.0.0.1:8188").rstrip("/")
WF = Path(os.getenv("WORKFLOWS_DIR", str(Path(__file__).parent / "workflows")))
ALFRED_RENDER_URL = os.getenv(
"ALFRED_RENDER_URL", "http://127.0.0.1:54321/api/render"
)
ALFRED_RENDER_TOKEN = os.getenv("ALFRED_RENDER_TOKEN", "").strip()
def submit(workflow: dict) -> str:
response = requests.post(
f"{BASE}/prompt",
json={"prompt": workflow, "client_id": str(uuid.uuid4())},
timeout=30,
)
response.raise_for_status()
return response.json()["prompt_id"]
def wait(prompt_id: str) -> dict:
deadline = time.monotonic() + int(os.getenv("COMFYUI_TIMEOUT", "300"))
while time.monotonic() < deadline:
response = requests.get(f"{BASE}/history/{prompt_id}", timeout=15)
response.raise_for_status()
result = response.json()
if prompt_id in result:
return result[prompt_id]
time.sleep(1)
raise TimeoutError(f"ComfyUI timeout ({prompt_id})")
def extract_images(history: dict) -> list[dict]:
"""Construit les URL de /view depuis les sorties ComfyUI (SaveImage)."""
images = []
for node_id, output in history.get("outputs", {}).items():
for image in output.get("images", []):
filename = image.get("filename")
if not filename:
continue
subfolder = image.get("subfolder", "")
image_type = image.get("type", "output")
query = urlencode(
{
"filename": filename,
"subfolder": subfolder,
"type": image_type,
}
)
images.append(
{
"node_id": str(node_id),
"filename": filename,
"subfolder": subfolder,
"type": image_type,
"url": f"{BASE}/view?{query}",
}
)
return images
def show_on_alfred(image: dict, caption: str) -> tuple[bool, str | None]:
"""Ne fait pas échouer la génération si Alfred est hors ligne."""
headers = {}
if ALFRED_RENDER_TOKEN:
headers["X-Alfred-Render-Token"] = ALFRED_RENDER_TOKEN
try:
response = requests.post(
ALFRED_RENDER_URL,
json={
"type": "image",
"url": image["url"],
"alt": "Image générée par ComfyUI",
"caption": caption,
},
headers=headers,
timeout=5,
)
response.raise_for_status()
return True, None
except requests.RequestException as exc:
return False, str(exc)
@mcp.tool()
def generate_image(prompt: str) -> dict:
"""Génère une image par ComfyUI et l'affiche automatiquement sur Alfred.
Utiliser cet outil lorsque l'utilisateur demande de créer/générer une image.
Ne pas prétendre que l'image a été affichée si displayed_on_alfred est faux.
"""
prompt = str(prompt).strip()
if not prompt:
raise ValueError("Le prompt de génération est vide")
workflow = json.loads((WF / "text_to_image.json").read_text(encoding="utf-8"))
prompt_node = os.getenv("COMFY_PROMPT_NODE", "6")
workflow[prompt_node]["inputs"]["text"] = prompt
prompt_id = submit(workflow)
history = wait(prompt_id)
images = extract_images(history)
if not images:
raise RuntimeError(
"ComfyUI a terminé sans image de sortie. "
"Vérifie le nœud SaveImage du workflow."
)
displayed, display_error = show_on_alfred(
images[0], f"Image générée : {prompt}"
)
return {
"ok": True,
"prompt_id": prompt_id,
"prompt": prompt,
"images": images,
"first_image_url": images[0]["url"],
"displayed_on_alfred": displayed,
"display_error": display_error,
}
@mcp.tool()
def analyze_image(image_path: str, question: str) -> dict:
"""Analyse une image via le workflow VLM configuré (inchangé)."""
workflow = json.loads((WF / "vision_analyze.json").read_text(encoding="utf-8"))
workflow[os.getenv("COMFY_VISION_IMAGE_NODE", "1")]["inputs"]["image"] = image_path
workflow[os.getenv("COMFY_VISION_PROMPT_NODE", "2")]["inputs"]["text"] = question
return wait(submit(workflow))
if __name__ == "__main__":
mcp.run()
+100
View File
@@ -0,0 +1,100 @@
import os, json, time, uuid, requests
from pathlib import Path
from urllib.parse import quote
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv()
mcp = FastMCP("Alfred ComfyUI")
BASE = os.getenv("COMFYUI_URL", "http://127.0.0.1:8188").rstrip("/")
WF = Path(os.getenv("WORKFLOWS_DIR", Path(__file__).parent / "workflows"))
def submit(workflow):
r = requests.post(
BASE + "/prompt",
json={
"prompt": workflow,
"client_id": str(uuid.uuid4())
},
timeout=30
)
r.raise_for_status()
return r.json()["prompt_id"]
def wait(pid):
deadline = time.time() + int(os.getenv("COMFYUI_TIMEOUT", "300"))
while time.time() < deadline:
h = requests.get(f"{BASE}/history/{pid}", timeout=15).json()
if pid in h:
return h[pid]
time.sleep(1)
raise TimeoutError("ComfyUI timeout")
def build_image_url(filename, subfolder="", img_type="output"):
return (
f"{BASE}/view?"
f"filename={quote(str(filename))}"
f"&subfolder={quote(str(subfolder))}"
f"&type={quote(str(img_type))}"
)
def extract_images(history):
images = []
outputs = history.get("outputs", {})
for node_id, node_output in outputs.items():
for img in node_output.get("images", []):
filename = img.get("filename")
subfolder = img.get("subfolder", "")
img_type = img.get("type", "output")
if not filename:
continue
images.append({
"node_id": node_id,
"filename": filename,
"subfolder": subfolder,
"type": img_type,
"url": build_image_url(filename, subfolder, img_type)
})
return images
@mcp.tool()
def generate_image(prompt: str):
"""Generate an image with the locked text_to_image workflow."""
w = json.loads((WF / "text_to_image.json").read_text())
node = os.getenv("COMFY_PROMPT_NODE", "6")
w[node]["inputs"]["text"] = prompt
history = wait(submit(w))
images = extract_images(history)
return {
"ok": True,
"message": f"Image générée pour : {prompt}",
"prompt": prompt,
"images": images,
"first_image_url": images[0]["url"] if images else None,
"first_image_filename": images[0]["filename"] if images else None
}
@mcp.tool()
def analyze_image(image_path: str, question: str):
"""Analyze a camera/local image using the locked VLM workflow. Workflow node IDs are configured in .env."""
w = json.loads((WF / "vision_analyze.json").read_text())
w[os.getenv("COMFY_VISION_IMAGE_NODE", "1")]["inputs"]["image"] = image_path
w[os.getenv("COMFY_VISION_PROMPT_NODE", "2")]["inputs"]["text"] = question
return wait(submit(w))
if __name__ == "__main__":
mcp.run()
+3
View File
@@ -0,0 +1,3 @@
mcp>=1.12
requests>=2.32
python-dotenv>=1.0
+1
View File
@@ -0,0 +1 @@
Exporter depuis ComfyUI les workflows API JSON et les nommer `text_to_image.json` et `vision_analyze.json`. Les IDs de nœuds sont configurables dans `.env`.
@@ -0,0 +1,107 @@
{
"3": {
"inputs": {
"seed": 156680208700286,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "absolutereality_v181.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "text, watermark",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
@@ -0,0 +1,107 @@
{
"3": {
"inputs": {
"seed": 156680208700286,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "absolutereality_v181.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "text, watermark",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
FACE_DB=/home/hermes/faces
FACE_TOLERANCE=0.48
CAMERA_INDEX=0
+3
View File
@@ -0,0 +1,3 @@
FACE_DB=/home/hermes/faces
FACE_TOLERANCE=0.48
CAMERA_INDEX=0
+3
View File
@@ -0,0 +1,3 @@
# MCP_face_recognition
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
@@ -0,0 +1,28 @@
import os, cv2, face_recognition
from pathlib import Path
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv();mcp=FastMCP("Alfred Face Recognition");DB=Path(os.getenv('FACE_DB','/home/hermes/faces'));T=float(os.getenv('FACE_TOLERANCE','0.48'))
def encodings():
known=[]
for person in DB.iterdir() if DB.exists() else []:
if not person.is_dir():continue
for f in person.glob('*'):
try:
im=face_recognition.load_image_file(str(f)); es=face_recognition.face_encodings(im)
if es:known.append((person.name,es[0]))
except Exception:pass
return known
@mcp.tool()
def identify_people(camera_index:int=0):
cap=cv2.VideoCapture(camera_index);ok,frame=cap.read();cap.release()
if not ok:raise RuntimeError('Caméra indisponible')
rgb=cv2.cvtColor(frame,cv2.COLOR_BGR2RGB); locs=face_recognition.face_locations(rgb); es=face_recognition.face_encodings(rgb,locs); known=encodings();out=[]
for e in es:
name='inconnu';best=T
for n,k in known:
d=face_recognition.face_distance([k],e)[0]
if d<best:name,best=n,d
out.append({'name':name,'distance':float(best)})
return {'count':len(out),'people':out,'unknown_count':sum(x['name']=='inconnu' for x in out)}
if __name__=='__main__':mcp.run()
@@ -0,0 +1,4 @@
mcp>=1.12
python-dotenv>=1.0
face-recognition>=1.3
opencv-python-headless>=4.10
+2
View File
@@ -0,0 +1,2 @@
READ_ROOTS=/Disques/Divers/
WRITE_ROOT=/home/hermes/alfred-data
+2
View File
@@ -0,0 +1,2 @@
READ_ROOTS=/mnt/nas/music:/mnt/nas/documents
WRITE_ROOT=/home/hermes/alfred-data
+3
View File
@@ -0,0 +1,3 @@
# MCP_files
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
+30
View File
@@ -0,0 +1,30 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv();mcp=FastMCP("Alfred Files")
READ=[Path(x).resolve() for x in os.getenv('READ_ROOTS','').split(':') if x]; WRITE=Path(os.getenv('WRITE_ROOT','/home/hermes/alfred-data')).resolve();WRITE.mkdir(parents=True,exist_ok=True)
def safe_read(p):
p=Path(p).resolve();
if not any(p==r or r in p.parents for r in READ): raise PermissionError('Hors des zones de lecture autorisées')
return p
def safe_write(name):
p=(WRITE/name).resolve();
if not (p==WRITE or WRITE in p.parents): raise PermissionError('Écriture hors workspace interdite')
return p
@mcp.tool()
def list_directory(path:str): return [{'name':p.name,'is_dir':p.is_dir()} for p in safe_read(path).iterdir()]
@mcp.tool()
def search_files(query:str):
out=[]
for r in READ:
for p in r.rglob('*'):
if query.lower() in p.name.lower(): out.append(str(p))
if len(out)>=100:return out
return out
@mcp.tool()
def read_text(path:str, max_chars:int=50000): return safe_read(path).read_text(errors='replace')[:max_chars]
@mcp.tool()
def write_workspace_file(name:str, content:str):
p=safe_write(name);p.parent.mkdir(parents=True,exist_ok=True);p.write_text(content);return str(p)
if __name__=='__main__':mcp.run()
+2
View File
@@ -0,0 +1,2 @@
mcp>=1.12
python-dotenv>=1.0
+3
View File
@@ -0,0 +1,3 @@
HA_URL=http://192.168.1.141:8123
HA_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI1N2M1MmY4OGViNDI0MmEzOWM4ZGVkYzY5NjA0ZjAyYiIsImlhdCI6MTc5MTI3NDY5OSwiZXhwIjoyMTA2NjM0Njk5fQ.JHFxqul7PxyoC8X9FgPYnEGYuuFycDw_Af4EOTytG7c
HA_ALLOWED_DOMAINS=light,switch,cover,climate,sensor,binary_sensor,media_player,calendar
+3
View File
@@ -0,0 +1,3 @@
HA_URL=http://homeassistant.local:8123
HA_TOKEN=CHANGE_ME
HA_ALLOWED_DOMAINS=light,switch,cover,climate,sensor,binary_sensor,media_player,calendar
+3
View File
@@ -0,0 +1,3 @@
# MCP_home_assistant
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
@@ -0,0 +1,245 @@
import os
from pathlib import Path
from typing import Any, Dict, Optional
import requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
HA_URL = os.getenv("HA_URL", "http://192.168.1.141:8123").rstrip("/")
HA_TOKEN = os.getenv("HA_TOKEN")
if not HA_TOKEN:
raise RuntimeError("HA_TOKEN manquant dans /opt/hermes/ha-mcp/.env")
HEADERS = {
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json",
}
mcp = FastMCP("home-assistant")
def ha_get(path: str) -> Any:
response = requests.get(
f"{HA_URL}{path}",
headers=HEADERS,
timeout=10,
)
response.raise_for_status()
return response.json()
def ha_post(path: str, payload: Optional[Dict[str, Any]] = None) -> Any:
response = requests.post(
f"{HA_URL}{path}",
headers=HEADERS,
json=payload or {},
timeout=10,
)
response.raise_for_status()
if response.text:
return response.json()
return {"status": "ok"}
@mcp.tool()
def ha_check_api() -> Any:
"""Check if Home Assistant API is reachable."""
return ha_get("/api/")
@mcp.tool()
def ha_list_entities(domain: Optional[str] = None) -> Any:
"""
List Home Assistant entities.
Optional domain examples:
- light
- switch
- sensor
- binary_sensor
- automation
- script
"""
states = ha_get("/api/states")
if domain:
prefix = f"{domain}."
states = [
entity for entity in states
if entity.get("entity_id", "").startswith(prefix)
]
return [
{
"entity_id": entity.get("entity_id"),
"state": entity.get("state"),
"friendly_name": entity.get("attributes", {}).get("friendly_name"),
}
for entity in states
]
@mcp.tool()
def ha_search_entities(search: str) -> Any:
"""
Search Home Assistant entities by entity_id or friendly name.
"""
search_lower = search.lower()
states = ha_get("/api/states")
results = []
for entity in states:
entity_id = entity.get("entity_id", "")
friendly_name = entity.get("attributes", {}).get("friendly_name", "")
if (
search_lower in entity_id.lower()
or search_lower in friendly_name.lower()
):
results.append(
{
"entity_id": entity_id,
"state": entity.get("state"),
"friendly_name": friendly_name,
}
)
return results
@mcp.tool()
def ha_get_state(entity_id: str) -> Any:
"""Get the current state of a Home Assistant entity."""
return ha_get(f"/api/states/{entity_id}")
@mcp.tool()
def ha_turn_on(entity_id: str) -> Any:
"""
Turn on a Home Assistant entity.
Works with domains such as:
- light
- switch
- fan
- input_boolean
"""
domain = entity_id.split(".")[0]
return ha_post(
f"/api/services/{domain}/turn_on",
{"entity_id": entity_id},
)
@mcp.tool()
def ha_turn_off(entity_id: str) -> Any:
"""
Turn off a Home Assistant entity.
Works with domains such as:
- light
- switch
- fan
- input_boolean
"""
domain = entity_id.split(".")[0]
return ha_post(
f"/api/services/{domain}/turn_off",
{"entity_id": entity_id},
)
@mcp.tool()
def ha_toggle(entity_id: str) -> Any:
"""Toggle a Home Assistant entity."""
domain = entity_id.split(".")[0]
return ha_post(
f"/api/services/{domain}/toggle",
{"entity_id": entity_id},
)
@mcp.tool()
def ha_call_service(
domain: str,
service: str,
data: Dict[str, Any],
) -> Any:
"""
Call any Home Assistant service.
Example:
domain = "light"
service = "turn_on"
data = {
"entity_id": "light.salon",
"brightness_pct": 50
}
"""
return ha_post(
f"/api/services/{domain}/{service}",
data,
)
@mcp.tool()
def ha_run_script(script_entity_id: str) -> Any:
"""Run a Home Assistant script entity."""
return ha_post(
"/api/services/script/turn_on",
{"entity_id": script_entity_id},
)
@mcp.tool()
def ha_trigger_automation(
automation_entity_id: str,
skip_condition: bool = True,
) -> Any:
"""
Trigger a Home Assistant automation manually.
By default, conditions are skipped.
"""
return ha_post(
"/api/services/automation/trigger",
{
"entity_id": automation_entity_id,
"skip_condition": skip_condition,
},
)
@mcp.tool()
def ha_restart_home_assistant() -> Any:
"""
Restart Home Assistant.
Use carefully.
"""
return ha_post("/api/services/homeassistant/restart")
@mcp.tool()
def ha_reload_automations() -> Any:
"""Reload Home Assistant automations."""
return ha_post("/api/services/automation/reload")
@mcp.tool()
def ha_reload_scripts() -> Any:
"""Reload Home Assistant scripts."""
return ha_post("/api/services/script/reload")
if __name__ == "__main__":
mcp.run()
@@ -0,0 +1,20 @@
import os, requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv(); mcp=FastMCP("Alfred Home Assistant")
BASE=os.getenv("HA_URL","").rstrip("/"); TOKEN=os.getenv("HA_TOKEN","")
def headers(): return {"Authorization":f"Bearer {TOKEN}","Content-Type":"application/json"}
@mcp.tool()
def get_state(entity_id:str):
"""Read one Home Assistant entity state exposed for Alfred."""
r=requests.get(f"{BASE}/api/states/{entity_id}",headers=headers(),timeout=15); r.raise_for_status(); return r.json()
@mcp.tool()
def call_service(domain:str, service:str, entity_id:str, data:dict|None=None):
"""Call an allowed HA service. Prefer entities explicitly exposed to Assist."""
allowed=set(x.strip() for x in os.getenv("HA_ALLOWED_DOMAINS","light,switch,cover,climate,media_player,calendar").split(','))
if domain not in allowed: raise ValueError("Domaine non autorisé")
payload={"entity_id":entity_id, **(data or {})}; r=requests.post(f"{BASE}/api/services/{domain}/{service}",headers=headers(),json=payload,timeout=20); r.raise_for_status(); return r.json()
@mcp.tool()
def list_states(domain:str=""):
r=requests.get(f"{BASE}/api/states",headers=headers(),timeout=20);r.raise_for_status();items=r.json();return [x for x in items if not domain or x['entity_id'].startswith(domain+'.')]
if __name__=='__main__': mcp.run()
@@ -0,0 +1,3 @@
mcp>=1.12
requests>=2.32
python-dotenv>=1.0
+2
View File
@@ -0,0 +1,2 @@
PAPERLESS_URL=https://paperless.nerkdesign.com/
PAPERLESS_TOKEN=4c55d54776783220375c24bd4701edbae24cec86
+2
View File
@@ -0,0 +1,2 @@
PAPERLESS_URL=http://paperless.local:8000
PAPERLESS_TOKEN=CHANGE_ME
+3
View File
@@ -0,0 +1,3 @@
# MCP_paperless
Copier `.env.example` vers `.env`, puis créer le venv et installer `requirements.txt`.
+11
View File
@@ -0,0 +1,11 @@
import os, requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
load_dotenv();mcp=FastMCP("Alfred Paperless");BASE=os.getenv('PAPERLESS_URL','').rstrip('/');TOKEN=os.getenv('PAPERLESS_TOKEN','');H={'Authorization':f'Token {TOKEN}'}
@mcp.tool()
def search_documents(query:str, limit:int=10):
r=requests.get(BASE+'/api/documents/',headers=H,params={'query':query,'page_size':limit},timeout=20);r.raise_for_status();return r.json()
@mcp.tool()
def get_document(document_id:int):
r=requests.get(f'{BASE}/api/documents/{document_id}/',headers=H,timeout=20);r.raise_for_status();return r.json()
if __name__=='__main__':mcp.run()
+3
View File
@@ -0,0 +1,3 @@
mcp>=1.12
requests>=2.32
python-dotenv>=1.0