Support external vault roots across tooling
This commit is contained in:
+48
-5
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP server for the knowledge-base repository.
|
||||
"""MCP server for a selected knowledge-base vault, using framework-owned tooling.
|
||||
|
||||
This server is the single interface an agent needs to work the vault over stdio
|
||||
with JSON-RPC 2.0. It exposes graph and note *querying* (graph_summary,
|
||||
with JSON-RPC 2.0. It provides framework operating instructions (read_instructions),
|
||||
and exposes graph and note *querying* (graph_summary,
|
||||
graph_search_nodes, graph_neighbors, read_note, refresh_graph), *writing*
|
||||
(write_note for text, capture_raw for verbatim bytes from a local file or an
|
||||
http(s) download) and *publishing* (edit_note, git_commit, git_push), so an
|
||||
@@ -18,6 +19,7 @@ It auto-detects the transport layer:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
@@ -30,6 +32,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from vault_root import FRAMEWORK_ROOT, parse_vault_args
|
||||
|
||||
SERVER_NAME = "knowledge-base-mcp"
|
||||
SERVER_VERSION = "0.1.0"
|
||||
@@ -227,6 +230,12 @@ def _relative_to_root(state: AppState, target: Path) -> str:
|
||||
|
||||
|
||||
def _run_git(state: AppState, args: List[str]) -> subprocess.CompletedProcess:
|
||||
repo = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"], cwd=state.root,
|
||||
check=False, capture_output=True, text=True,
|
||||
)
|
||||
if repo.returncode != 0 or Path(repo.stdout.strip()).resolve() != state.root.resolve():
|
||||
raise ValueError("vault root must be the Git repository root for Git operations")
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
cwd=state.root,
|
||||
@@ -249,6 +258,19 @@ def _git_result(payload: Dict[str, Any], exit_code: int) -> Dict[str, Any]:
|
||||
|
||||
def _tool_definitions() -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"name": "read_instructions",
|
||||
"description": (
|
||||
"Read framework AGENTS.md and AGENT_QUERY_GUIDE.md before working "
|
||||
"with the vault. These operating documents live outside an "
|
||||
"external vault and do not require a generated graph."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "graph_summary",
|
||||
"description": (
|
||||
@@ -474,6 +496,15 @@ def _text_result(text: str, structured: Dict[str, Any] | None = None) -> Dict[st
|
||||
return result
|
||||
|
||||
|
||||
def _tool_read_instructions() -> Dict[str, Any]:
|
||||
documents = [
|
||||
{"path": name, "content": (FRAMEWORK_ROOT / name).read_text(encoding="utf-8")}
|
||||
for name in ("AGENTS.md", "AGENT_QUERY_GUIDE.md")
|
||||
]
|
||||
payload = {"framework_root": str(FRAMEWORK_ROOT), "documents": documents}
|
||||
return _text_result(json.dumps(payload, indent=2), structured=payload)
|
||||
|
||||
|
||||
def _tool_graph_summary(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
graph = _load_graph(state)
|
||||
nodes = graph.get("nodes", [])
|
||||
@@ -624,7 +655,10 @@ def _tool_read_note(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _tool_refresh_graph(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
command = [sys.executable, "scripts/generate-graph.py"]
|
||||
command = [
|
||||
sys.executable, str(FRAMEWORK_ROOT / "scripts/generate-graph.py"),
|
||||
"--vault-root", str(state.root), "--write",
|
||||
]
|
||||
if bool(args.get("validate", False)):
|
||||
command.append("--validate")
|
||||
|
||||
@@ -884,6 +918,8 @@ def _tool_git_push(state: AppState, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _call_tool(state: AppState, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if name == "read_instructions":
|
||||
return _tool_read_instructions()
|
||||
if name == "graph_summary":
|
||||
return _tool_graph_summary(state, arguments)
|
||||
if name == "graph_search_nodes":
|
||||
@@ -908,7 +944,7 @@ def _call_tool(state: AppState, name: str, arguments: Dict[str, Any]) -> Dict[st
|
||||
|
||||
|
||||
def _list_resources(state: AppState) -> Dict[str, Any]:
|
||||
return {
|
||||
result = {
|
||||
"resources": [
|
||||
{
|
||||
"uri": "repo://graph",
|
||||
@@ -924,6 +960,12 @@ def _list_resources(state: AppState) -> Dict[str, Any]:
|
||||
},
|
||||
]
|
||||
}
|
||||
if not (state.root / "README.md").is_file():
|
||||
result["resources"] = [
|
||||
resource for resource in result["resources"]
|
||||
if resource["uri"] != "repo://readme"
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def _read_resource(state: AppState, uri: str) -> Dict[str, Any]:
|
||||
@@ -994,7 +1036,8 @@ def _handle_request(state: AppState, message: Dict[str, Any]) -> Dict[str, Any]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
args = parse_vault_args(argparse.ArgumentParser(description=__doc__))
|
||||
root = args.vault_root
|
||||
state = AppState(root=root, graph_path=root / "index" / "graph.json")
|
||||
|
||||
reader = _StdinReader(sys.stdin.buffer)
|
||||
|
||||
Reference in New Issue
Block a user