diff --git a/scripts/manage_mcp_service.py b/scripts/manage_mcp_service.py index a4a6c91..50d096c 100755 --- a/scripts/manage_mcp_service.py +++ b/scripts/manage_mcp_service.py @@ -30,6 +30,7 @@ DEFAULT_PORT = 8765 DEFAULT_PATH = "/mcp" DEFAULT_TOKEN_BYTES = 32 DEFAULT_UV_CACHE_DIR = "/tmp/uv-cache" +DEFAULT_OPENCODE_PATH = "opencode" TOOL_FILE_ASSETS = "file-assets" TOOL_INGEST = "ingest" AVAILABLE_TOOL_GROUPS = (TOOL_FILE_ASSETS, TOOL_INGEST) @@ -113,6 +114,18 @@ def find_uv_path() -> Path: return Path(discovered).expanduser().resolve() +def resolve_opencode_path(configured: str | Path | None = None) -> str: + value = str(configured or "").strip() + if value: + configured_path = Path(value).expanduser() + if configured_path.is_absolute() or configured_path.parent != Path("."): + return str(configured_path.resolve()) + discovered = shutil.which(value) + return str(Path(discovered).resolve()) if discovered else value + discovered = shutil.which(DEFAULT_OPENCODE_PATH) + return str(Path(discovered).resolve()) if discovered else DEFAULT_OPENCODE_PATH + + def resolve_service_paths(project_root: Path, config_home: Path | None = None) -> ServicePaths: root = project_root.expanduser().resolve() base_config = config_home or Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) @@ -215,6 +228,7 @@ def write_token_env_file( path: str = DEFAULT_PATH, enabled_tools: Sequence[str] | None = None, enable_ingest: bool | None = None, + opencode_path: str | Path | None = None, ) -> None: tools = normalize_enabled_tools(DEFAULT_ENABLED_TOOLS if enabled_tools is None else enabled_tools) tools = update_ingest_tool(tools, enable_ingest) @@ -228,6 +242,8 @@ def write_token_env_file( env_file.write(f"OBSIDIAN_MCP_PATH={path}\n") env_file.write(f"OBSIDIAN_MCP_ENABLED_TOOLS={format_enabled_tools(tools)}\n") env_file.write(f"OBSIDIAN_MCP_ENABLE_INGEST={1 if TOOL_INGEST in tools else 0}\n") + if opencode_path: + env_file.write(f"OBSIDIAN_MCP_INGEST_OPENCODE_PATH={opencode_path}\n") env_path.chmod(0o600) @@ -246,7 +262,7 @@ def read_service_env_file(env_path: Path) -> dict[str, str]: return values -def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[str, ...]]: +def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[str, ...], str]: paths = resolve_service_paths(project_root) values = read_service_env_file(paths.env_path) token = values.get("OBSIDIAN_MCP_TOKEN", "").strip() @@ -256,7 +272,8 @@ def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[ port = int(values.get("OBSIDIAN_MCP_PORT") or DEFAULT_PORT) path = values.get("OBSIDIAN_MCP_PATH") or DEFAULT_PATH enabled_tools = enabled_tools_from_config(values) - return token, host, port, path, enabled_tools + opencode_path = values.get("OBSIDIAN_MCP_INGEST_OPENCODE_PATH", "").strip() + return token, host, port, path, enabled_tools, opencode_path def write_service_unit( @@ -352,7 +369,7 @@ def render_remote_config( def show_config(project_root: Path) -> str: paths = resolve_service_paths(project_root) - token, _host, port, path, enabled_tools = saved_service_config(project_root) + token, _host, port, path, enabled_tools, _opencode_path = saved_service_config(project_root) return render_remote_config( hosts=detect_remote_hosts(), token=token, @@ -384,12 +401,21 @@ def new_token( ) -> str: paths = resolve_service_paths(project_root) enabled_tools = DEFAULT_ENABLED_TOOLS + opencode_path = "" try: - _old_token, _old_host, _old_port, _old_path, enabled_tools = saved_service_config(project_root) + _old_token, _old_host, _old_port, _old_path, enabled_tools, opencode_path = saved_service_config(project_root) except RuntimeError: pass token = generate_token(token_bytes) - write_token_env_file(paths.env_path, token, host=host, port=port, path=path, enabled_tools=enabled_tools) + write_token_env_file( + paths.env_path, + token, + host=host, + port=port, + path=path, + enabled_tools=enabled_tools, + opencode_path=opencode_path, + ) restart_service_if_installed(paths) return render_remote_config( hosts=detect_remote_hosts(), @@ -413,14 +439,25 @@ def start_service( ) -> str: paths = resolve_service_paths(project_root) try: - token, host, port, path, enabled_tools = saved_service_config(project_root) + token, host, port, path, enabled_tools, opencode_path = saved_service_config(project_root) except RuntimeError: if paths.env_path.exists(): raise token = generate_token(token_bytes) enabled_tools = DEFAULT_ENABLED_TOOLS + opencode_path = "" enabled_tools = update_ingest_tool(enabled_tools, enable_ingest) - write_token_env_file(paths.env_path, token, host=host, port=port, path=path, enabled_tools=enabled_tools) + if TOOL_INGEST in enabled_tools and not opencode_path: + opencode_path = resolve_opencode_path() + write_token_env_file( + paths.env_path, + token, + host=host, + port=port, + path=path, + enabled_tools=enabled_tools, + opencode_path=opencode_path, + ) uv_path = find_uv_path() prepare_runtime(paths.project_root, uv_path=uv_path) resolved_vault = resolve_vault_for_service(paths.project_root, vault_path=vault_path) @@ -462,35 +499,46 @@ def render_tool_status( return "\n".join(lines) + "\n" -def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, tuple[str, ...]]: +def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, tuple[str, ...], str]: paths = resolve_service_paths(project_root) try: - token, host, port, path, enabled_tools = saved_service_config(project_root) + token, host, port, path, enabled_tools, opencode_path = saved_service_config(project_root) except RuntimeError: token = generate_token() host = DEFAULT_HOST port = DEFAULT_PORT path = DEFAULT_PATH enabled_tools = DEFAULT_ENABLED_TOOLS + opencode_path = "" write_token_env_file(paths.env_path, token, host=host, port=port, path=path, enabled_tools=enabled_tools) - return paths, token, host, port, path, enabled_tools + return paths, token, host, port, path, enabled_tools, opencode_path def list_tools(project_root: Path) -> str: - paths, _token, _host, _port, _path, enabled_tools = ensure_service_config(project_root) + paths, _token, _host, _port, _path, enabled_tools, _opencode_path = ensure_service_config(project_root) return render_tool_status(config_path=paths.env_path, enabled_tools=enabled_tools) def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str: tool_name = normalize_tool_name(tool) - paths, token, host, port, path, enabled_tools = ensure_service_config(project_root) + paths, token, host, port, path, enabled_tools, opencode_path = ensure_service_config(project_root) current = set(enabled_tools) if enabled: current.add(tool_name) else: current.discard(tool_name) updated_tools = tuple(item for item in AVAILABLE_TOOL_GROUPS if item in current) - write_token_env_file(paths.env_path, token, host=host, port=port, path=path, enabled_tools=updated_tools) + if TOOL_INGEST in updated_tools and not opencode_path: + opencode_path = resolve_opencode_path() + write_token_env_file( + paths.env_path, + token, + host=host, + port=port, + path=path, + enabled_tools=updated_tools, + opencode_path=opencode_path, + ) restarted = restart_service_if_installed(paths) return render_tool_status(config_path=paths.env_path, enabled_tools=updated_tools, restarted=restarted) diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index bff3390..2446329 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -199,6 +199,7 @@ class McpServiceConfigTests(unittest.TestCase): 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.shutil, "which", return_value="/home/nac/.opencode/bin/opencode"), patch.object(manage_mcp_service, "prepare_runtime") as prepare_runtime, patch.object(manage_mcp_service, "run_systemctl") as run_systemctl, ): @@ -217,6 +218,7 @@ class McpServiceConfigTests(unittest.TestCase): self.assertIn("OBSIDIAN_MCP_PATH=/wiki-mcp\n", env_content) self.assertIn("OBSIDIAN_MCP_ENABLED_TOOLS=file-assets,ingest\n", env_content) self.assertIn("OBSIDIAN_MCP_ENABLE_INGEST=1\n", env_content) + self.assertIn("OBSIDIAN_MCP_INGEST_OPENCODE_PATH=/home/nac/.opencode/bin/opencode\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) @@ -242,6 +244,12 @@ class McpServiceConfigTests(unittest.TestCase): path="/mcp", enabled_tools=("file-assets", "ingest"), ) + env_path = project_root / ".runtime" / "obsidian-wiki-mcp.env" + env_path.write_text( + env_path.read_text(encoding="utf-8") + + "OBSIDIAN_MCP_INGEST_OPENCODE_PATH=/custom/bin/opencode\n", + encoding="utf-8", + ) with ( patch.dict("os.environ", {"XDG_CONFIG_HOME": str(config_home)}), @@ -256,6 +264,7 @@ class McpServiceConfigTests(unittest.TestCase): env_content = (project_root / ".runtime" / "obsidian-wiki-mcp.env").read_text(encoding="utf-8") self.assertIn("OBSIDIAN_MCP_ENABLED_TOOLS=file-assets,ingest\n", env_content) self.assertIn("OBSIDIAN_MCP_ENABLE_INGEST=1\n", env_content) + self.assertIn("OBSIDIAN_MCP_INGEST_OPENCODE_PATH=/custom/bin/opencode\n", env_content) def test_main_tools_enable_disable_updates_enabled_tools_in_config(self): with tempfile.TemporaryDirectory() as tmp: