Files
mnote/scripts/mnote-lightrag-mcp-server.py
T

184 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""MNote LightRAG MCP facade.
This is intentionally thin: LightRAG owns retrieval, MNote owns citation/open
mapping through its source registry.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
DEFAULT_ENV_FILE = "/mnt/Data1T/Mnote_data/lightrag/LightRAG/.env"
DEFAULT_MNOTE_WEB_URL = "http://127.0.0.1:3000"
DEFAULT_ROOT_URI = "file:///mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space"
DEFAULT_WORKSPACE_ID = "local-ws:mnote-e2e:my-space"
mcp = FastMCP("MNote-LightRAG-Server")
def _read_env_value(key: str, env_file: str) -> str:
path = Path(env_file)
if not path.exists():
return ""
for line in path.read_text(encoding="utf-8").splitlines():
if not line.startswith(f"{key}="):
continue
value = line.split("=", 1)[1].strip()
if (value.startswith('"') and value.endswith('"')) or (
value.startswith("'") and value.endswith("'")
):
value = value[1:-1]
return value
return ""
def _lightrag_base_url() -> str:
env_file = os.environ.get("MNOTE_LIGHTRAG_ENV_FILE", DEFAULT_ENV_FILE)
host = os.environ.get("LIGHTRAG_HOST") or _read_env_value("HOST", env_file) or "127.0.0.1"
port = os.environ.get("LIGHTRAG_PORT") or _read_env_value("PORT", env_file) or "9621"
return f"http://{host}:{port}".rstrip("/")
def _lightrag_api_key() -> str:
env_file = os.environ.get("MNOTE_LIGHTRAG_ENV_FILE", DEFAULT_ENV_FILE)
return os.environ.get("LIGHTRAG_API_KEY") or _read_env_value("LIGHTRAG_API_KEY", env_file)
def _mnote_web_url() -> str:
return os.environ.get("MNOTE_WEB_URL", DEFAULT_MNOTE_WEB_URL).rstrip("/")
def _mnote_headers() -> dict[str, str]:
return {
"content-type": "application/json",
"x-mnote-actor-id": os.environ.get("MNOTE_ACTOR_ID", "mnote-e2e"),
"x-mnote-actor-type": os.environ.get("MNOTE_ACTOR_TYPE", "user"),
}
async def _request_lightrag(path: str, *, method: str = "GET", json_body: Any = None) -> Any:
headers = {"accept": "application/json"}
api_key = _lightrag_api_key()
if api_key:
# 当前 LightRAG /query 接受 X-API-KeyBearer 在本机版本会返回 Invalid token。
headers["X-API-Key"] = api_key
async with httpx.AsyncClient(timeout=180) as client:
response = await client.request(
method,
f"{_lightrag_base_url()}{path}",
headers=headers,
json=json_body,
)
try:
payload = response.json()
except Exception:
payload = {"text": response.text}
if response.status_code >= 400:
return {"status": "error", "response": None, "error": payload, "httpStatus": response.status_code}
return {"status": "success", "response": payload, "error": None, "httpStatus": response.status_code}
@mcp.tool(
name="verify_server_health",
description="Check whether the configured local LightRAG server is healthy.",
)
async def verify_server_health() -> Any:
return await _request_lightrag("/health")
@mcp.tool(
name="check_indexing_status",
description="Check the LightRAG document processing pipeline status.",
)
async def check_indexing_status() -> Any:
return await _request_lightrag("/documents/pipeline_status")
@mcp.tool(
name="list_all_docs",
description="List documents currently known to LightRAG.",
)
async def list_all_docs() -> Any:
return await _request_lightrag("/documents")
@mcp.tool(
name="query_knowledge_graph",
description="Search the local LightRAG knowledge base. Use mix by default; references include provider file_path/chunk_id for MNote citation mapping.",
)
async def query_knowledge_graph(
prompt: str,
search_mode: str = "mix",
limit: int = 60,
context_only: bool = False,
prompt_only: bool = False,
include_references: bool = True,
include_chunk_content: bool = True,
) -> Any:
mode_aliases = {
"keyword": "naive",
"semantic": "hybrid",
}
mode = mode_aliases.get(search_mode, search_mode)
body = {
"query": prompt,
"mode": mode,
"top_k": limit,
"chunk_top_k": limit,
"only_need_context": context_only,
"only_need_prompt": prompt_only,
"include_references": include_references,
"include_chunk_content": include_chunk_content,
"response_type": "Multiple Paragraphs",
}
return await _request_lightrag("/query", method="POST", json_body=body)
@mcp.tool(
name="open_mnote_reference",
description="Map a LightRAG reference/file_path/chunk_id to an MNote clickable citationUrl using MNote source registry.",
)
async def open_mnote_reference(
file_path: str,
chunk_id: str = "",
reference_id: str = "",
workspace_id: str = DEFAULT_WORKSPACE_ID,
root_uri: str = DEFAULT_ROOT_URI,
include_registry: bool = False,
) -> Any:
body: dict[str, Any] = {
"workspaceId": workspace_id,
"rootUri": root_uri,
"filePath": file_path,
}
if chunk_id:
body["chunkId"] = chunk_id
if reference_id:
body["referenceId"] = reference_id
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
f"{_mnote_web_url()}/api/knowledge-rag/open-reference",
headers=_mnote_headers(),
json=body,
)
try:
payload = response.json()
except Exception:
payload = {"text": response.text}
if response.status_code >= 400:
return {"status": "error", "response": None, "error": payload, "httpStatus": response.status_code}
if not include_registry and isinstance(payload, dict):
payload = {key: value for key, value in payload.items() if key != "registry"}
return {"status": "success", "response": payload, "error": None, "httpStatus": response.status_code}
if __name__ == "__main__":
mcp.run()