Return HTML charts as EmbeddedResource instead of server-side files

HTML charts were being written to the server filesystem and returning
a path the client couldn't access. Now returns the HTML content
inline as an MCP EmbeddedResource with text/html MIME type, so the
client receives the full interactive chart over the wire.
This commit is contained in:
Ryan Malloy 2026-02-23 19:50:36 -07:00
parent 89cdeb0967
commit ad17d72894
2 changed files with 21 additions and 29 deletions

View File

@ -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/

View File

@ -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,
),
)