diff --git a/Dockerfile.mcp b/Dockerfile.mcp index f499316..5ca7746 100644 --- a/Dockerfile.mcp +++ b/Dockerfile.mcp @@ -15,7 +15,6 @@ WORKDIR /app ENV UV_COMPILE_BYTECODE=1 ENV PATH="/app/.venv/bin:$PATH" ENV MPLCONFIGDIR=/tmp/matplotlib -ENV MCNOAA_CHARTS_DIR=/tmp/charts COPY --from=deps /app/.venv /app/.venv COPY src/ src/ diff --git a/src/mcnoaa_tides/tools/charts.py b/src/mcnoaa_tides/tools/charts.py index 3dbb5da..aa6d73a 100644 --- a/src/mcnoaa_tides/tools/charts.py +++ b/src/mcnoaa_tides/tools/charts.py @@ -2,12 +2,11 @@ import asyncio from datetime import datetime, timezone -from pathlib import Path from typing import Literal from fastmcp import Context, FastMCP from fastmcp.utilities.types import Image -from mcp.types import ToolAnnotations +from mcp.types import EmbeddedResource, TextResourceContents, ToolAnnotations from mcnoaa_tides.charts import check_deps from mcnoaa_tides.client import NOAAClient @@ -23,15 +22,16 @@ def register(mcp: FastMCP) -> None: hours: int = 48, include_observed: bool = True, format: Literal["png", "html"] = "png", - ) -> Image | str: + ) -> Image | EmbeddedResource: """Generate a tide prediction chart with high/low markers. Creates a visual chart of tide predictions showing the water level curve with high (H) and low (L) tide markers. Optionally overlays observed water levels as a dashed line for comparison. - PNG format returns an inline image. HTML format saves an interactive - chart to artifacts/charts/ and returns the file path. + PNG format returns an inline image. HTML format returns an interactive + Plotly chart as an embedded resource (text/html) that the client can + save or render directly. Requires mcnoaa-tides[viz] to be installed. """ @@ -97,8 +97,7 @@ def register(mcp: FastMCP) -> None: from mcnoaa_tides.charts.tides import render_tide_chart_html html = render_tide_chart_html(predictions, observed, station_name) - path = _save_html(html, station_id, "tides") - return f"Interactive tide chart saved to: {path}" + return _html_resource(html, station_id, "tides") @mcp.tool(tags={"visualization"}, annotations=_ANNOTATIONS) async def visualize_conditions( @@ -106,7 +105,7 @@ def register(mcp: FastMCP) -> None: station_id: str, hours: int = 24, format: Literal["png", "html"] = "png", - ) -> Image | str: + ) -> Image | EmbeddedResource: """Generate a multi-panel marine conditions dashboard. Creates a dashboard with up to 4 panels: @@ -117,8 +116,9 @@ def register(mcp: FastMCP) -> None: Products unavailable at a station are simply omitted from the dashboard. - PNG format returns an inline image. HTML format saves an interactive - chart to artifacts/charts/ and returns the file path. + PNG format returns an inline image. HTML format returns an interactive + Plotly dashboard as an embedded resource (text/html) that the client + can save or render directly. Requires mcnoaa-tides[viz] to be installed. """ @@ -184,23 +184,16 @@ def register(mcp: FastMCP) -> None: from mcnoaa_tides.charts.conditions import render_conditions_html html = render_conditions_html(snapshot, station_name) - path = _save_html(html, station_id, "conditions") - return f"Interactive conditions dashboard saved to: {path}" + return _html_resource(html, station_id, "conditions") -def _save_html(html: str, station_id: str, chart_type: str) -> Path: - """Save HTML chart and return the path. - - Uses $MCNOAA_CHARTS_DIR if set, otherwise falls back to - artifacts/charts/ (relative to cwd). The container sets - MCNOAA_CHARTS_DIR=/tmp/charts so the nobody user can write. - """ - import os - - timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") - base = os.environ.get("MCNOAA_CHARTS_DIR", "artifacts/charts") - out_dir = Path(base) - out_dir.mkdir(parents=True, exist_ok=True) - path = out_dir / f"{station_id}_{chart_type}_{timestamp}.html" - path.write_text(html, encoding="utf-8") - return path +def _html_resource(html: str, station_id: str, chart_type: str) -> EmbeddedResource: + """Wrap an HTML chart as an MCP EmbeddedResource for inline delivery.""" + return EmbeddedResource( + type="resource", + resource=TextResourceContents( + uri=f"chart://{chart_type}/{station_id}", + mimeType="text/html", + text=html, + ), + )