diff --git a/README.md b/README.md
index 8141b56..874d62e 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets AI
## Features
-- **15 tools** for complete TV control via natural language
+- **18 tools** for complete TV control via natural language
- **Auto-discovery** — finds Samsung TVs on your network via SSDP
- **Zero config** — no API keys, no cloud, everything runs locally
- **Multi-protocol** — combines WebSocket, REST, UPnP/DLNA, and Wake-on-LAN
@@ -28,6 +28,9 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets AI
| `tv_apps` | List all installed apps |
| `tv_launch` | Launch app by name ("netflix") or ID |
| `tv_close_app` | Close a running app |
+| `tv_current_app` | Detect which app is currently running |
+| `tv_aspect_ratio` | Get/set aspect ratio (Default, 16:9, Zoom, 4:3) |
+| `tv_captions` | Get caption state or toggle subtitles |
| `tv_browser` | Open a URL in the TV's browser |
| `tv_text` | Type text into active input fields |
| `tv_cursor` | Move virtual cursor on screen |
diff --git a/main.py b/main.py
index 6ced0c3..fbecf9e 100644
--- a/main.py
+++ b/main.py
@@ -250,6 +250,64 @@ async def tv_close_app(app: str) -> dict[str, Any]:
return await _safe(_do)
+# ── Current App ─────────────────────────────────────────────
+
+
+@mcp.tool()
+async def tv_current_app() -> dict[str, Any]:
+ """Detect which app is currently running on the TV.
+
+ Checks all known apps via REST API. Returns the running app's
+ name, ID, and visibility, or a message if no known app is active.
+ """
+ def _do():
+ app = _tv.current_app()
+ if app:
+ return _ok(f"Running: {app['name']}", **app)
+ return _ok("No known app is currently running")
+ return await _safe(_do, timeout=40)
+
+
+# ── Aspect Ratio ────────────────────────────────────────────
+
+
+@mcp.tool()
+async def tv_aspect_ratio(ratio: Optional[str] = None) -> dict[str, Any]:
+ """Get or set the TV's aspect ratio.
+
+ Args:
+ ratio: Set to this value. Known values: "Default", "16:9", "Zoom",
+ "4:3", "Screen Fit". Omit to just read current ratio.
+ """
+ def _do():
+ if ratio:
+ _tv.set_aspect_ratio(ratio)
+ return _ok(f"Aspect ratio set to {ratio}", ratio=ratio)
+ current = _tv.get_aspect_ratio()
+ return _ok(f"Aspect ratio: {current}", ratio=current)
+ return await _safe(_do)
+
+
+# ── Captions ────────────────────────────────────────────────
+
+
+@mcp.tool()
+async def tv_captions(toggle: bool = False) -> dict[str, Any]:
+ """Get caption/subtitle state or toggle captions on/off.
+
+ Args:
+ toggle: If True, sends the CAPTION key to toggle subtitles.
+ If False (default), just returns current caption state.
+ """
+ def _do():
+ if toggle:
+ _tv.send_key("KEY_CAPTION")
+ return _ok("Caption toggled")
+ state = _tv.get_captions()
+ return _ok("Caption state", **state)
+ return await _safe(_do)
+
+
# ── Browser & Text ───────────────────────────────────────────────
diff --git a/tv.py b/tv.py
index a276db4..39c9c16 100644
--- a/tv.py
+++ b/tv.py
@@ -430,6 +430,59 @@ class SamsungTV:
raise ValueError(f"Unknown: '{action}'. Valid: play, pause, stop, seek, status")
return {"action": a, "done": True}
+ # ── App Detection ────────────────────────────────────────
+
+ def current_app(self) -> dict[str, Any] | None:
+ """Detect which known app is currently running via REST."""
+ ip = self._ensure_ip()
+ for name, ids in APP_ALIASES.items():
+ try:
+ raw = urlopen(
+ f"http://{ip}:8001/api/v2/applications/{ids[0]}",
+ timeout=2,
+ ).read()
+ data = json.loads(raw)
+ if data.get("running"):
+ return {
+ "name": name,
+ "id": ids[0],
+ "visible": data.get("visible", False),
+ }
+ except Exception:
+ continue
+ return None
+
+ # ── Aspect Ratio ──────────────────────────────────────────
+
+ def get_aspect_ratio(self) -> str:
+ ip = self._ensure_ip()
+ xml = _soap_call(
+ ip, "/upnp/control/RenderingControl1", "RenderingControl",
+ "X_GetAspectRatio", "0",
+ )
+ return _soap_value(xml, "AspectRatio") or "Unknown"
+
+ def set_aspect_ratio(self, ratio: str) -> None:
+ ip = self._ensure_ip()
+ _soap_call(
+ ip, "/upnp/control/RenderingControl1", "RenderingControl",
+ "X_SetAspectRatio",
+ f"0{ratio}",
+ )
+
+ # ── Captions ──────────────────────────────────────────────
+
+ def get_captions(self) -> dict[str, str]:
+ ip = self._ensure_ip()
+ xml = _soap_call(
+ ip, "/upnp/control/RenderingControl1", "RenderingControl",
+ "X_GetCaptionState", "0",
+ )
+ return {
+ "captions": _soap_value(xml, "Captions") or "",
+ "enabled": _soap_value(xml, "EnabledCaptions") or "",
+ }
+
def close(self) -> None:
if self._ws:
try: