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.
This commit is contained in:
Codex
2026-07-05 11:44:24 +00:00
parent baa8c0beae
commit d543831b5e
4 changed files with 72 additions and 7 deletions
+2 -1
View File
@@ -41,8 +41,9 @@ OBSIDIAN_MCP_EXPOSE_PATHS=false
# Run locally:
# OBSIDIAN_MCP_TOKEN='<long-random-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
+13 -2
View File
@@ -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:
+27 -4
View File
@@ -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()
+30
View File
@@ -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"