Add independent MCP PDF report delivery

This commit is contained in:
2026-09-27 11:09:13 +02:00
parent 0c18c86a47
commit 6c36d78063
9 changed files with 555 additions and 20 deletions
+90 -4
View File
@@ -1,15 +1,19 @@
"""RollCalc's minimal stdio MCP server."""
"""RollCalc MCP server with stdio and Streamable HTTP transports."""
from __future__ import annotations
import os
from typing import Any
from urllib.parse import quote
from mcp_pdf_delivery import McpReportNotFoundError, McpReportStore
from rollcalc_mcp_tools import (
analyze_transport_capacity_result,
calculate_machine_max_product_length_result,
calculate_material_weight_result,
calculate_product_length_result,
calculate_roll_diameter_result,
generate_calculation_pdf_result,
get_article_result,
get_machine_result,
check_production_feasibility_result,
@@ -18,11 +22,49 @@ from rollcalc_mcp_tools import (
from transport_calculation import TransportPresetKey
async def download_report(request):
"""Serve one MCP-host-local report by opaque temporary capability."""
from starlette.responses import PlainTextResponse, Response
token = request.path_params["token"]
try:
pdf, report = McpReportStore().read(token)
except McpReportNotFoundError:
return PlainTextResponse("Report not found or expired", status_code=404)
return Response(
pdf,
media_type="application/pdf",
headers={
"Content-Disposition": (
f'attachment; filename="{quote(report.filename)}"'
),
},
)
def selected_transport() -> str:
"""Read and validate the transport selected for this MCP process."""
transport = os.getenv("ROLLCALC_MCP_TRANSPORT", "stdio").strip().lower()
if transport not in {"stdio", "streamable-http"}:
raise ValueError(
"ROLLCALC_MCP_TRANSPORT must be 'stdio' or 'streamable-http'"
)
return transport
def create_server() -> Any:
"""Create the stdio-only MCP server after the SDK is installed."""
"""Create the transport-neutral RollCalc MCP application."""
from mcp.server.fastmcp import FastMCP
server = FastMCP("RollCalc")
server = FastMCP(
"RollCalc",
host=os.getenv("ROLLCALC_MCP_HOST", "127.0.0.1"),
port=int(os.getenv("ROLLCALC_MCP_PORT", "8000")),
)
@server.custom_route("/reports/{token}.pdf", methods=["GET"])
async def mcp_report_download_route(request):
return await download_report(request)
@server.tool()
def get_article(
@@ -145,6 +187,45 @@ def create_server() -> Any:
production_site=production_site,
)
@server.tool()
def generate_calculation_pdf(
roll_length_m: float | None = None,
core_diameter_mm: float | None = None,
thickness_mm: float | None = None,
thickness_stddev_mm: float | None = None,
article_number: str | None = None,
article_name_hint: str | None = None,
core_type: str | None = None,
width_m: float | None = None,
area_weight_g_m2: float | None = None,
category: str | None = None,
production_site: str | None = None,
include_roll_weight: bool = True,
) -> dict[str, Any]:
"""Create a PDF calculation report only when the user explicitly requests a PDF or report. Supply the original inputs from a completed calculate_roll_diameter request; never supply, infer, or copy calculated diameters, weights, warnings, or notes. The tool recalculates authoritatively and returns a short-lived browser URL hosted by the MCP service. Do not call this tool after ordinary calculations and do not offer it proactively."""
if selected_transport() != "streamable-http":
return {
"status": "artifact_delivery_unavailable",
"error": (
"generate_calculation_pdf requires "
"ROLLCALC_MCP_TRANSPORT=streamable-http"
),
}
return generate_calculation_pdf_result(
article_number=article_number,
article_name_hint=article_name_hint,
roll_length_m=roll_length_m,
width_m=width_m,
thickness_mm=thickness_mm,
thickness_stddev_mm=thickness_stddev_mm,
area_weight_g_m2=area_weight_g_m2,
core_diameter_mm=core_diameter_mm,
core_type=core_type,
category=category,
production_site=production_site,
include_roll_weight=include_roll_weight,
)
@server.tool()
def analyze_transport_capacity(
transport_preset: TransportPresetKey,
@@ -167,5 +248,10 @@ def create_server() -> Any:
return server
def run_server() -> None:
"""Run the selected FastMCP transport without changing tool definitions."""
create_server().run(transport=selected_transport())
if __name__ == "__main__":
create_server().run(transport="stdio")
run_server()