From d543831b5e4cf4cd71c43d53f39bbbbed085e9b9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 11:44:24 +0000 Subject: [PATCH] feat: print remote config on MCP service start Make scripts/mcp_service.sh start create the token env file when missing, then start the user service and print the remote MCP URL, Authorization header, and client JSON. Document the one-command startup flow and cover automatic token creation with tests. --- .env.example | 3 ++- README.md | 15 +++++++++++++-- scripts/manage_mcp_service.py | 31 +++++++++++++++++++++++++++---- tests/test_mcp_service.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 41a7dd0..950e409 100644 --- a/.env.example +++ b/.env.example @@ -41,8 +41,9 @@ OBSIDIAN_MCP_EXPOSE_PATHS=false # Run locally: # OBSIDIAN_MCP_TOKEN='' obsidian-wiki-mcp --transport streamable-http # Or manage a user-level service: -# bash scripts/mcp_service.sh new-token # bash scripts/mcp_service.sh start +# Rotate token later with: +# bash scripts/mcp_service.sh new-token OBSIDIAN_MCP_TOKEN= OBSIDIAN_MCP_HOST=127.0.0.1 OBSIDIAN_MCP_PORT=8765 diff --git a/README.md b/README.md index 008f667..15474ae 100644 --- a/README.md +++ b/README.md @@ -364,11 +364,22 @@ Client configuration example: For long-running access from another machine, use the managed user service: ```bash -bash scripts/mcp_service.sh new-token bash scripts/mcp_service.sh start ``` -`mcp_service.sh new-token` writes `.runtime/obsidian-wiki-mcp.env` with `0600` permissions and prints the remote URL, `Authorization` header, and client JSON. `mcp_service.sh start` writes `~/.config/systemd/user/obsidian-wiki-mcp.service` and starts it. The service defaults to `0.0.0.0:8765/mcp` because it is intended for remote clients, but it still requires the bearer token. Prefer SSH tunnel, Tailscale, WireGuard, or another trusted network boundary instead of directly exposing the port to the public internet. +`mcp_service.sh start` creates `.runtime/obsidian-wiki-mcp.env` with `0600` permissions if it does not exist, writes `~/.config/systemd/user/obsidian-wiki-mcp.service`, starts the service, and prints the remote URL, `Authorization` header, and client JSON to copy into another agent. The service defaults to `0.0.0.0:8765/mcp` because it is intended for remote clients, but it still requires the bearer token. Prefer SSH tunnel, Tailscale, WireGuard, or another trusted network boundary instead of directly exposing the port to the public internet. + +Customize the first startup if needed: + +```bash +bash scripts/mcp_service.sh start --host 0.0.0.0 --port 9876 --path /wiki-mcp +``` + +Rotate the token later with: + +```bash +bash scripts/mcp_service.sh new-token +``` Useful service commands: diff --git a/scripts/manage_mcp_service.py b/scripts/manage_mcp_service.py index 67a77d6..a5cf949 100755 --- a/scripts/manage_mcp_service.py +++ b/scripts/manage_mcp_service.py @@ -124,7 +124,7 @@ def write_token_env_file( def read_service_env_file(env_path: Path) -> dict[str, str]: if not env_path.exists(): - raise RuntimeError(f"Missing MCP service env file: {env_path}. Run scripts/mcp_service.sh new-token first.") + raise RuntimeError(f"Missing MCP service env file: {env_path}. Run scripts/mcp_service.sh start to create it.") values: dict[str, str] = {} for raw_line in env_path.read_text(encoding="utf-8").splitlines(): @@ -262,9 +262,19 @@ def new_token( def start_service( *, project_root: Path, + host: str = DEFAULT_HOST, + port: int = DEFAULT_PORT, + path: str = DEFAULT_PATH, + token_bytes: int = DEFAULT_TOKEN_BYTES, ) -> str: paths = resolve_service_paths(project_root) - token, host, port, path = saved_service_config(project_root) + try: + token, host, port, path = saved_service_config(project_root) + except RuntimeError: + if paths.env_path.exists(): + raise + token = generate_token(token_bytes) + write_token_env_file(paths.env_path, token, host=host, port=port, path=path) write_service_unit(paths, host=host, port=port, path=path) run_systemctl(["daemon-reload"]) @@ -307,8 +317,12 @@ def main(argv: Sequence[str] | None = None) -> int: new_token_parser.add_argument("--path", default=DEFAULT_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.") + start_parser = subparsers.add_parser("start", help="Install or update the user service and print remote MCP config.") start_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path) + start_parser.add_argument("--host", default=DEFAULT_HOST) + start_parser.add_argument("--port", default=DEFAULT_PORT, type=int) + start_parser.add_argument("--path", default=DEFAULT_PATH) + start_parser.add_argument("--token-bytes", default=DEFAULT_TOKEN_BYTES, type=int) subparsers.add_parser("stop", help="Stop the user service without deleting service files.") @@ -335,7 +349,16 @@ def main(argv: Sequence[str] | None = None) -> int: ) return 0 if args.command == "start": - print(start_service(project_root=args.project_root), end="") + print( + start_service( + project_root=args.project_root, + host=args.host, + port=args.port, + path=args.path, + token_bytes=args.token_bytes, + ), + end="", + ) return 0 if args.command == "stop": stop_service() diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index 248b2db..18180b9 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -152,6 +152,36 @@ class McpServiceConfigTests(unittest.TestCase): ] ) + def test_start_service_generates_token_when_missing_and_prints_config(self): + with tempfile.TemporaryDirectory() as tmp: + project_root = Path(tmp) / "project" + project_root.mkdir() + config_home = Path(tmp) / "config" + with ( + patch.dict("os.environ", {"XDG_CONFIG_HOME": str(config_home)}), + patch.object(manage_mcp_service, "generate_token", return_value="created-token"), + patch.object(manage_mcp_service, "detect_remote_hosts", return_value=["10.0.0.4"]), + patch.object(manage_mcp_service, "find_uv_path", return_value=Path("/home/nac/.local/bin/uv")), + patch.object(manage_mcp_service, "run_systemctl") as run_systemctl, + ): + output = manage_mcp_service.start_service(project_root=project_root, host="0.0.0.0", port=9876, path="/wiki-mcp") + + env_path = project_root / ".runtime" / "obsidian-wiki-mcp.env" + env_content = env_path.read_text(encoding="utf-8") + self.assertIn("OBSIDIAN_MCP_TOKEN=created-token\n", env_content) + self.assertIn("OBSIDIAN_MCP_PORT=9876\n", env_content) + self.assertIn("OBSIDIAN_MCP_PATH=/wiki-mcp\n", env_content) + self.assertIn("http://10.0.0.4:9876/wiki-mcp", output) + self.assertIn("Authorization: Bearer created-token", output) + self.assertIn('"type": "streamable-http"', output) + run_systemctl.assert_has_calls( + [ + call(["daemon-reload"]), + call(["enable", manage_mcp_service.SERVICE_NAME]), + call(["restart", manage_mcp_service.SERVICE_NAME]), + ] + ) + def test_uninstall_service_removes_only_managed_files(self): with tempfile.TemporaryDirectory() as tmp: project_root = Path(tmp) / "project"