feat(mcp): separate direct order queries from wiki tools

Add an opt-in business group with schema, template, guarded SQL, and status tools. Reuse existing order statistics through read-only SQLite connections without OpenCode or wiki retrieval.

Add tools set for switching enabled groups with one restart. Gate file diagnostics with file-assets and document business-only operation while retaining the independent Kingdee sync task.

Validation: 217 unit tests passed; business tool tests rerun after read-only annotations. Live authenticated MCP schema/status/template/SQL calls passed, ask_wiki was unavailable, uploads returned 404, and unauthenticated requests returned 401.
This commit is contained in:
Codex
2026-10-31 06:47:43 +00:00
parent 1133e2bf50
commit 6c451c3eb6
10 changed files with 500 additions and 44 deletions
+29 -1
View File
@@ -53,7 +53,8 @@ DEFAULT_RUNTIME_CLEANUP_DAYS = 30
TOOL_FILE_ASSETS = "file-assets"
TOOL_INGEST = "ingest"
TOOL_QUERY = "query"
AVAILABLE_TOOL_GROUPS = (TOOL_FILE_ASSETS, TOOL_INGEST, TOOL_QUERY)
TOOL_BUSINESS = "business"
AVAILABLE_TOOL_GROUPS = (TOOL_FILE_ASSETS, TOOL_INGEST, TOOL_QUERY, TOOL_BUSINESS)
DEFAULT_ENABLED_TOOLS = (TOOL_FILE_ASSETS,)
TERMINAL_JOB_STATES = {"succeeded", "failed", "cancelled"}
PROXY_ENV_NAMES = ("http_proxy", "https_proxy", "all_proxy", "no_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY")
@@ -71,6 +72,8 @@ def normalize_tool_name(name: str) -> str:
"upload": TOOL_INGEST,
"uploads": TOOL_INGEST,
"opencode-ingest": TOOL_INGEST,
"business": TOOL_BUSINESS,
"orders": TOOL_BUSINESS,
"query": TOOL_QUERY,
"wiki-query": TOOL_QUERY,
"ask": TOOL_QUERY,
@@ -642,6 +645,12 @@ def render_remote_config(
"Remote agents should upload with create_ingest_upload_ticket and X-Obsidian-Wiki-Upload-Token.\n"
"Bearer upload is for local/admin use. POST multipart/form-data with one or more `files` fields.\n"
)
business_section = ""
if enabled_tools is not None and TOOL_BUSINESS in normalize_enabled_tools(enabled_tools):
business_section = (
"\nBusiness query tools (direct SQLite, no OpenCode):\n"
"Use get_business_schema, query_business_template, execute_business_sql, and get_business_status.\n"
)
query_section = ""
if enabled_tools is not None and TOOL_QUERY in normalize_enabled_tools(enabled_tools):
query_section = (
@@ -661,6 +670,7 @@ def render_remote_config(
f"{enabled_section}"
f"{ingest_upload_section}"
f"{query_section}"
f"{business_section}"
"\n"
"Use this header:\n"
f"Authorization: Bearer {token}\n"
@@ -1000,6 +1010,17 @@ def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str:
else:
current.discard(tool_name)
updated_tools = tuple(item for item in AVAILABLE_TOOL_GROUPS if item in current)
return _save_tool_groups(paths, config, updated_tools)
def set_enabled_tools(project_root: Path, tools: Sequence[str]) -> str:
"""Replace the whole group set with one config write and one service restart."""
updated_tools = normalize_enabled_tools(tools)
paths, config = ensure_service_config(project_root)
return _save_tool_groups(paths, config, updated_tools)
def _save_tool_groups(paths: ServicePaths, config: ServiceConfig, updated_tools: tuple[str, ...]) -> str:
opencode_path = config.opencode_path
if ({TOOL_INGEST, TOOL_QUERY} & set(updated_tools)) and not opencode_path:
opencode_path = resolve_opencode_path()
@@ -1257,6 +1278,10 @@ def main(argv: Sequence[str] | None = None) -> int:
tools_disable_parser.add_argument("tool", choices=AVAILABLE_TOOL_GROUPS)
tools_disable_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
tools_set_parser = tools_subparsers.add_parser("set", help="Replace enabled groups and restart once; e.g. tools set business.")
tools_set_parser.add_argument("tools", nargs="+", choices=AVAILABLE_TOOL_GROUPS)
tools_set_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
args = parser.parse_args(argv)
if args.command == "new-token":
@@ -1309,6 +1334,9 @@ def main(argv: Sequence[str] | None = None) -> int:
print(cleanup_runtime(args.project_root, max_age_days=args.max_age_days), end="")
return 0
if args.command == "tools":
if args.tools_command == "set":
print(set_enabled_tools(args.project_root, args.tools), end="")
return 0
if args.tools_command == "list":
print(list_tools(args.project_root), end="")
return 0