Files
python-sdk/pkg/hanzo-tools-shell/hanzo_tools/shell/open_tool.py
T
zeekay bd9c6e5740 fix: async audit improvements and test fixes
- Fix deprecated asyncio.get_event_loop() → get_running_loop() in open_tool.py
- Add documentation for ProcessManager dict modification pattern
- Add module docstring for hanzo_async.paths explaining executor usage
- Fix bash test to use proper bash syntax (not zsh-style { } backgrounding)
- Bump hanzo-tools-shell 0.5.2 → 0.5.3
- Bump hanzo-async 0.1.0 → 0.1.1

All 53 shell tests pass. Async code review: production-ready.
2026-01-04 17:18:57 -08:00

117 lines
4.1 KiB
Python

"""Open files or URLs in the default application."""
import asyncio
import platform
import webbrowser
from typing import override
from pathlib import Path
from urllib.parse import urlparse
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.core import BaseTool, auto_timeout
class OpenTool(BaseTool):
"""Tool for opening files or URLs in the default application."""
name = "open"
def register(self, server: FastMCP) -> None:
"""Register the tool with the MCP server."""
tool_self = self
@server.tool(name=self.name, description=self.description)
async def open(path: str, ctx: MCPContext) -> str:
return await tool_self.run(ctx, path)
@auto_timeout("open")
async def call(self, ctx: MCPContext, **params) -> str:
"""Call the tool with arguments."""
return await self.run(ctx, params["path"])
@property
@override
def description(self) -> str:
"""Get the tool description."""
return """Open files or URLs. Platform-aware.
Usage:
open https://example.com
open ./document.pdf
open /path/to/image.png"""
async def _run_opener(self, cmd: list[str], file_path: str) -> bool:
"""Run an opener command asynchronously (fire-and-forget pattern)."""
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
try:
await asyncio.wait_for(proc.wait(), timeout=5)
return proc.returncode == 0
except asyncio.TimeoutError:
# Process is still running, which is fine for opener commands
return True
except Exception:
return False
async def run(self, ctx: MCPContext, path: str) -> str:
"""Open a file or URL in the default application."""
parsed = urlparse(path)
is_url = parsed.scheme in ("http", "https", "ftp", "file")
if is_url:
try:
# Run webbrowser.open in executor to avoid blocking
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, webbrowser.open, path)
return f"Opened URL in browser: {path}"
except Exception as e:
raise RuntimeError(f"Failed to open URL: {e}")
file_path = Path(path).expanduser().resolve()
# Check file exists asynchronously
loop = asyncio.get_running_loop()
exists = await loop.run_in_executor(None, file_path.exists)
if not exists:
raise RuntimeError(f"File not found: {file_path}")
system = platform.system().lower()
try:
if system == "darwin": # macOS
if await self._run_opener(["open", str(file_path)], str(file_path)):
return f"Opened file: {file_path}"
raise RuntimeError("Failed to open file with 'open' command")
elif system == "linux":
# Try xdg-open first
if await self._run_opener(["xdg-open", str(file_path)], str(file_path)):
return f"Opened file: {file_path}"
# Fallback to other openers
for opener in ["gnome-open", "kde-open", "exo-open"]:
if await self._run_opener([opener, str(file_path)], str(file_path)):
return f"Opened file: {file_path}"
raise RuntimeError("No suitable file opener found on Linux")
elif system == "windows":
import os
# Run os.startfile in executor to avoid blocking
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, os.startfile, str(file_path))
return f"Opened file: {file_path}"
else:
raise RuntimeError(f"Unsupported platform: {system}")
except RuntimeError:
raise
except Exception as e:
raise RuntimeError(f"Error opening file: {e}")
# Create tool instance
open_tool = OpenTool()