Files
pp-qa-km/mcp/server.py
LittleYellow cb2c92452e
Some checks failed
kb-lint / lint (push) Has been cancelled
Phase 5: MCP 薄殼(search_wiki/read_page)+ BM25 檢索 + 三個 Copilot Custom Agents
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:05:01 +08:00

57 lines
2.2 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.
"""FastMCP 薄殼AGENTS.md §8search_wiki 與 read_page 兩個 tool。
查詢時只讀已編譯的 wiki 頁——不觸碰 raw 層、不做全庫掃描。
啟動stdio.venv/Scripts/python mcp/server.py
測試時可用環境變數 PP_QA_ROOT 指定專案根目錄。
"""
import os
import pathlib
import sys
ROOT = pathlib.Path(os.environ.get("PP_QA_ROOT",
pathlib.Path(__file__).resolve().parents[1]))
sys.path.insert(0, str(ROOT / "tools"))
import kb
import search as searchmod
from fastmcp import FastMCP
mcp = FastMCP("pp-qa-knowledge")
def _truncate(body, max_tokens):
# ponytail: token 估算用「1 token ≈ 1.5 字元」的 naive heuristic
# (繁中約 1 字/token、英文約 4 字元/token 的折衷);
# 升級路徑:以實際模型 tokenizer 校正。
limit = int(max_tokens * 1.5)
return (body, False) if len(body) <= limit else (body[:limit], True)
def search_wiki(query: str, top_k: int = 10) -> list:
"""BM25 檢索 wiki 頁,回傳 index 條目path/title/description/tags+ 相關度 score。
query 用繁體中文或英文關鍵詞。"""
return searchmod.search(ROOT, query, max(1, min(int(top_k), 10))) # 上限 10AGENTS.md §8
def read_page(path: str) -> dict:
"""讀取單一 wiki 頁全文frontmatter + 內文(依 models.yaml token 上限截斷)
+ source_refs。path 例wiki/entities/payment-gateway.md"""
cfg = kb.load_config(ROOT)
p = (ROOT / path).resolve()
wiki = (ROOT / "wiki").resolve()
# 信任邊界:擋路徑跳脫,只允許 wiki/ 下的 .md
if not p.is_relative_to(wiki) or p.suffix != ".md":
raise ValueError("path 須指向 wiki/ 下的 .md 頁面")
if not p.is_file():
raise FileNotFoundError(f"頁面不存在:{path}")
meta, body = kb.parse_page(p.read_text(encoding="utf-8"))
body, truncated = _truncate(body, cfg["limits"]["mcp_response_max_tokens"])
return {"frontmatter": meta, "body": body, "truncated": truncated,
"source_refs": [str(s) for s in meta.get("sources") or []]}
mcp.tool(search_wiki)
mcp.tool(read_page)
if __name__ == "__main__":
mcp.run()