feat: use each user's local NocoBase login
This commit is contained in:
@@ -13,14 +13,13 @@ import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
SERVICE_NAME = "nocobase-crm-mcp.service"
|
||||
ENV_RELATIVE_PATH = Path(".runtime") / "nocobase-crm-mcp.env"
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 10087
|
||||
DEFAULT_PATH = "/mcp"
|
||||
DEFAULT_NB_ENV = "nb-mcp"
|
||||
@@ -80,7 +79,9 @@ def find_nb_path() -> Path:
|
||||
raise RuntimeError(f"NOCOBASE_MCP_NB_BIN points to {candidate_path}, but it is not an executable file.")
|
||||
discovered = shutil.which(candidate)
|
||||
if not discovered:
|
||||
raise RuntimeError("Could not find nb on PATH. Install the NocoBase CLI or set NOCOBASE_MCP_NB_BIN to its absolute path.")
|
||||
raise RuntimeError(
|
||||
"Could not find nb on PATH. Install the NocoBase CLI or set NOCOBASE_MCP_NB_BIN to its absolute path."
|
||||
)
|
||||
return Path(os.path.abspath(str(Path(discovered).expanduser())))
|
||||
|
||||
|
||||
@@ -213,9 +214,13 @@ def saved_service_config(project_root: Path) -> tuple[str, str, int, str, str, s
|
||||
return token, host, port, path, nb_env, config_path, nb_bin
|
||||
|
||||
|
||||
def write_service_unit(paths: ServicePaths, *, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, path: str = DEFAULT_PATH) -> None:
|
||||
def write_service_unit(
|
||||
paths: ServicePaths, *, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, path: str = DEFAULT_PATH
|
||||
) -> None:
|
||||
paths.unit_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
unit = render_service_unit(project_root=paths.project_root, env_file=paths.env_path, host=host, port=port, path=path)
|
||||
unit = render_service_unit(
|
||||
project_root=paths.project_root, env_file=paths.env_path, host=host, port=port, path=path
|
||||
)
|
||||
paths.unit_path.write_text(unit, encoding="utf-8")
|
||||
|
||||
|
||||
@@ -250,7 +255,7 @@ def render_remote_config(
|
||||
path: str = DEFAULT_PATH,
|
||||
service_status: str | None = None,
|
||||
) -> str:
|
||||
host = hosts[0] if hosts else "<server-ip>"
|
||||
host = hosts[0] if hosts else "127.0.0.1"
|
||||
url = f"http://{host}:{port}{path}"
|
||||
alternatives = "\n".join(f"- http://{candidate}:{port}{path}" for candidate in hosts[1:])
|
||||
alternatives_section = f"\nOther detected addresses:\n{alternatives}\n" if alternatives else ""
|
||||
@@ -259,7 +264,7 @@ def render_remote_config(
|
||||
"Saved MCP client config.\n"
|
||||
"\n"
|
||||
f"{status_section}"
|
||||
"Use this URL from another machine:\n"
|
||||
"Use this loopback URL on this machine:\n"
|
||||
f"{url}\n"
|
||||
f"{alternatives_section}"
|
||||
"\n"
|
||||
@@ -282,9 +287,10 @@ def render_remote_config(
|
||||
|
||||
|
||||
def show_config(project_root: Path) -> str:
|
||||
token, _host, port, path, _nb_env, _config_path, _nb_bin = saved_service_config(project_root)
|
||||
token, host, port, path, _nb_env, _config_path, _nb_bin = saved_service_config(project_root)
|
||||
display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
|
||||
return render_remote_config(
|
||||
hosts=detect_remote_hosts(),
|
||||
hosts=[display_host],
|
||||
token=token,
|
||||
port=port,
|
||||
path=path,
|
||||
@@ -372,7 +378,8 @@ def new_token(
|
||||
nb_bin=find_nb_path(),
|
||||
)
|
||||
restart_service_if_installed(paths)
|
||||
return render_remote_config(hosts=detect_remote_hosts(), token=token, port=port, path=path, service_status=service_state())
|
||||
display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
|
||||
return render_remote_config(hosts=[display_host], token=token, port=port, path=path, service_status=service_state())
|
||||
|
||||
|
||||
def start_service(*, project_root: Path) -> str:
|
||||
@@ -411,7 +418,8 @@ def start_service(*, project_root: Path) -> str:
|
||||
probe = wait_for_http_service("127.0.0.1", port, path, token=token)
|
||||
if not probe.get("reachable"):
|
||||
raise RuntimeError(f"{SERVICE_NAME} is active but HTTP probe failed: {probe.get('error')}")
|
||||
return render_remote_config(hosts=detect_remote_hosts(), token=token, port=port, path=path, service_status="active")
|
||||
display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
|
||||
return render_remote_config(hosts=[display_host], token=token, port=port, path=path, service_status="active")
|
||||
|
||||
|
||||
def stop_service() -> None:
|
||||
@@ -443,11 +451,11 @@ def service_status(*, project_root: Path) -> str:
|
||||
]
|
||||
if paths.env_path.exists():
|
||||
values = read_service_env_file(paths.env_path)
|
||||
token, _host, port, path, _nb_env, _config_path, nb_bin = saved_service_config(project_root)
|
||||
hosts = detect_remote_hosts()
|
||||
host = hosts[0] if hosts else "<server-ip>"
|
||||
lines.append(f"MCP URL: http://{host}:{port}{path}")
|
||||
token, host, port, path, nb_env, _config_path, nb_bin = saved_service_config(project_root)
|
||||
display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
|
||||
lines.append(f"MCP URL: http://{display_host}:{port}{path}")
|
||||
lines.append(f"nb binary: {nb_bin}")
|
||||
lines.append(f"NocoBase CLI env: {nb_env} (authenticate with `nb env auth {nb_env}`)")
|
||||
canonical_configured = bool(values.get(API_BASE_URL_ENV) and values.get(API_TOKEN_ENV))
|
||||
legacy_configured = bool(values.get(LEGACY_API_BASE_URL_ENV) and values.get(LEGACY_API_TOKEN_ENV))
|
||||
if canonical_configured:
|
||||
@@ -460,8 +468,8 @@ def service_status(*, project_root: Path) -> str:
|
||||
):
|
||||
credential_state = "incomplete"
|
||||
else:
|
||||
credential_state = "not configured"
|
||||
lines.append(f"NocoBase API credentials: {credential_state}")
|
||||
credential_state = "not configured; using the local nb CLI login"
|
||||
lines.append(f"Legacy NocoBase credential override: {credential_state}")
|
||||
probe = probe_http_service("127.0.0.1", port, path, token=token)
|
||||
if probe.get("reachable"):
|
||||
lines.append(f"HTTP probe: reachable (status {probe.get('status')})")
|
||||
@@ -477,7 +485,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Manage the HTTP NocoBase CRM MCP user service.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
new_token_parser = subparsers.add_parser("new-token", help="Generate a new token and print remote MCP client configuration.")
|
||||
new_token_parser = subparsers.add_parser(
|
||||
"new-token", help="Generate a new token and print local MCP client configuration."
|
||||
)
|
||||
new_token_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
|
||||
new_token_parser.add_argument("--host", default=DEFAULT_HOST)
|
||||
new_token_parser.add_argument("--port", default=DEFAULT_PORT, type=int)
|
||||
@@ -486,15 +496,21 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
new_token_parser.add_argument("--config-path", default=DEFAULT_CONFIG_PATH)
|
||||
new_token_parser.add_argument("--token-bytes", default=DEFAULT_TOKEN_BYTES, type=int)
|
||||
|
||||
start_parser = subparsers.add_parser("start", help="Install or update the user service using the saved token, then start it.")
|
||||
start_parser = subparsers.add_parser(
|
||||
"start", help="Install or update the user service using the saved token, then start it."
|
||||
)
|
||||
start_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
|
||||
|
||||
subparsers.add_parser("stop", help="Stop the user service without deleting service files.")
|
||||
|
||||
uninstall_parser = subparsers.add_parser("uninstall", help="Stop and remove the managed user service and token file.")
|
||||
uninstall_parser = subparsers.add_parser(
|
||||
"uninstall", help="Stop and remove the managed user service and token file."
|
||||
)
|
||||
uninstall_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
|
||||
|
||||
show_config_parser = subparsers.add_parser("show-config", help="Print the saved token and remote MCP client configuration.")
|
||||
show_config_parser = subparsers.add_parser(
|
||||
"show-config", help="Print the saved token and local MCP client configuration."
|
||||
)
|
||||
show_config_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
|
||||
|
||||
status_parser = subparsers.add_parser("status", help="Show systemd status for the managed service.")
|
||||
|
||||
Reference in New Issue
Block a user