diff --git a/docs/mcp-server.md b/docs/mcp-server.md index 12a21e5..4298018 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -35,18 +35,27 @@ You should see `schaeffler` listed with status "connected". | Tool | Description | |------|-------------| -| `query_database` | Run read-only SQL against PostgreSQL (SELECT only) | +| **Orders** | | | `list_orders` | List recent orders with render progress | | `get_order_detail` | Get full order detail with all lines | -| `search_products` | Search products by name, PIM-ID, category | -| `check_materials` | Find unmapped materials in an order | -| `list_materials` | List all library materials (with optional aliases) | | `dispatch_renders` | Dispatch/retry renders for an order | | `set_material_override` | Set material override on all order lines | +| **Products & Renders** | | +| `search_products` | Search products by name, PIM-ID, category | +| `get_product_detail` | Full product detail: metadata, parts, materials, render history, media assets | +| `get_render_detail` | Full render metadata for a specific order line: timing, engine, log | +| `get_completed_renders` | List completed renders with timing, file paths, filterable by product/order/output type | +| `get_failed_renders` | List recently failed renders with error messages | +| `get_media_assets` | List media assets (renders, thumbnails, GLBs, USDs) with metadata | +| **Materials** | | +| `check_materials` | Find unmapped materials in an order | +| `list_materials` | List all library materials (with optional aliases) | +| **System** | | | `list_output_types` | List all output types with settings | | `get_worker_activity` | Recent STEP processing and render tasks | | `get_render_stats` | Dashboard stats: throughput, coverage, counts | | `get_queue_status` | Live render queue depth and worker status | +| `query_database` | Run read-only SQL against PostgreSQL (SELECT only) | ## Available Resources diff --git a/schaeffler_mcp_server.py b/schaeffler_mcp_server.py index 85376e2..9f998bd 100644 --- a/schaeffler_mcp_server.py +++ b/schaeffler_mcp_server.py @@ -315,6 +315,210 @@ def get_queue_status() -> str: return json.dumps(rows[0] if rows else {}, indent=2) +@mcp.tool() +def get_product_detail(product_id: str) -> str: + """Get detailed information about a product: metadata, CAD file, parts, materials, render history. + + Args: + product_id: UUID of the product. + """ + rows = _db_query(""" + SELECT p.id, p.name, p.pim_id, p.category_key, p.ebene1, p.ebene2, + p.baureihe, p.produkt_baureihe, p.lagertyp, p.name_cad_modell, + p.is_active, p.cad_file_id, + p.cad_part_materials, p.components, + cf.original_name AS step_filename, + cf.processing_status AS step_status, + cf.parsed_objects, + cf.file_hash AS step_hash, + p.created_at, p.updated_at + FROM products p + LEFT JOIN cad_files cf ON cf.id = p.cad_file_id + WHERE p.id = %s + """, (product_id,)) + if not rows: + return f"Product {product_id} not found." + + product = rows[0] + + # Get render history + renders = _db_query(""" + SELECT ol.id AS line_id, ol.render_status, ol.result_path, + ol.material_override, ol.render_started_at, ol.render_completed_at, + ol.render_backend_used, + ot.name AS output_type_name, + o.order_number, + ol.render_log->>'engine_used' AS engine, + ol.render_log->>'render_duration_s' AS render_duration_s, + ol.render_log->>'total_duration_s' AS total_duration_s + FROM order_lines ol + JOIN orders o ON o.id = ol.order_id + LEFT JOIN output_types ot ON ot.id = ol.output_type_id + WHERE ol.product_id = %s + ORDER BY ol.render_completed_at DESC NULLS LAST + LIMIT 20 + """, (product_id,)) + + # Get media assets + assets = _db_query(""" + SELECT id, asset_type, storage_key, file_size_bytes, mime_type, created_at + FROM media_assets + WHERE product_id = %s + ORDER BY created_at DESC + LIMIT 20 + """, (product_id,)) + + product["renders"] = renders + product["media_assets"] = assets + return json.dumps(product, indent=2, default=str) + + +@mcp.tool() +def get_render_detail(order_line_id: str) -> str: + """Get full render metadata for a specific order line: timing, engine, materials, log. + + Args: + order_line_id: UUID of the order line. + """ + rows = _db_query(""" + SELECT ol.id, ol.order_id, ol.product_id, ol.output_type_id, + ol.render_status, ol.result_path, ol.material_override, + ol.render_started_at, ol.render_completed_at, + ol.render_backend_used, ol.render_log, + ol.unit_price, ol.item_status, + p.name AS product_name, p.pim_id, + ot.name AS output_type_name, ot.renderer, ot.output_format, + ot.render_settings, ot.material_override AS ot_material_override, + o.order_number + FROM order_lines ol + JOIN products p ON p.id = ol.product_id + LEFT JOIN output_types ot ON ot.id = ol.output_type_id + JOIN orders o ON o.id = ol.order_id + WHERE ol.id = %s + """, (order_line_id,)) + if not rows: + return f"Order line {order_line_id} not found." + return json.dumps(rows[0], indent=2, default=str) + + +@mcp.tool() +def get_completed_renders( + product_name: str = "", + order_number: str = "", + output_type: str = "", + limit: int = 20, +) -> str: + """List completed renders with metadata: product, output type, timing, file path. + + Args: + product_name: Filter by product name (partial match). + order_number: Filter by order number (partial match). + output_type: Filter by output type name (partial match). + limit: Max results (default 20, max 100). + """ + limit = min(limit, 100) + conditions = ["ol.render_status = 'completed'"] + params: list = [] + + if product_name: + conditions.append("p.name ILIKE %s") + params.append(f"%{product_name}%") + if order_number: + conditions.append("o.order_number ILIKE %s") + params.append(f"%{order_number}%") + if output_type: + conditions.append("ot.name ILIKE %s") + params.append(f"%{output_type}%") + + where = " AND ".join(conditions) + rows = _db_query(f""" + SELECT ol.id AS line_id, o.order_number, + p.name AS product_name, p.pim_id, + ot.name AS output_type_name, ot.output_format, + ol.result_path, ol.material_override, + ol.render_started_at, ol.render_completed_at, + ol.render_backend_used, + ol.render_log->>'engine_used' AS engine, + ol.render_log->>'render_duration_s' AS render_duration_s, + ol.render_log->>'total_duration_s' AS total_duration_s, + ol.render_log->>'output_size_bytes' AS output_size_bytes + FROM order_lines ol + JOIN products p ON p.id = ol.product_id + LEFT JOIN output_types ot ON ot.id = ol.output_type_id + JOIN orders o ON o.id = ol.order_id + WHERE {where} + ORDER BY ol.render_completed_at DESC + LIMIT %s + """, tuple(params) + (limit,)) + return json.dumps(rows, indent=2, default=str) + + +@mcp.tool() +def get_media_assets( + product_id: str = "", + asset_type: str = "", + limit: int = 30, +) -> str: + """List media assets (renders, thumbnails, GLBs, USDs) with metadata. + + Args: + product_id: Filter by product UUID. + asset_type: Filter by type (thumbnail, still, turntable, gltf_geometry, usd_master, blend_production). + limit: Max results (default 30, max 100). + """ + limit = min(limit, 100) + conditions = ["1=1"] + params: list = [] + + if product_id: + conditions.append("ma.product_id = %s") + params.append(product_id) + if asset_type: + conditions.append("ma.asset_type = %s") + params.append(asset_type) + + where = " AND ".join(conditions) + rows = _db_query(f""" + SELECT ma.id, ma.asset_type, ma.storage_key, + ma.file_size_bytes, ma.mime_type, + ma.product_id, p.name AS product_name, p.pim_id, + ma.order_line_id, ma.cad_file_id, + ma.created_at + FROM media_assets ma + LEFT JOIN products p ON p.id = ma.product_id + WHERE {where} + ORDER BY ma.created_at DESC + LIMIT %s + """, tuple(params) + (limit,)) + return json.dumps(rows, indent=2, default=str) + + +@mcp.tool() +def get_failed_renders(limit: int = 20) -> str: + """List recently failed renders with error details. + + Args: + limit: Max results (default 20). + """ + rows = _db_query(""" + SELECT ol.id AS line_id, o.order_number, + p.name AS product_name, p.pim_id, + ot.name AS output_type_name, + ol.render_completed_at AS failed_at, + ol.render_log->>'error' AS error, + ol.render_log->>'engine_used' AS engine, + ol.render_backend_used + FROM order_lines ol + JOIN products p ON p.id = ol.product_id + LEFT JOIN output_types ot ON ot.id = ol.output_type_id + JOIN orders o ON o.id = ol.order_id + WHERE ol.render_status = 'failed' + ORDER BY ol.render_completed_at DESC NULLS LAST + LIMIT %s + """, (min(limit, 50),)) + return json.dumps(rows, indent=2, default=str) + + # ── Resources ────────────────────────────────────────────────────────────────