MCP-Frappe/frappe_mcp/tools/analytics.py

177 lines
6.5 KiB
Python

"""
Level 6 — Reporting and analytics tools.
Aggregate queries, dashboard data, PDF rendering.
"""
import json
from mcp.types import Tool
from frappe_mcp.client.frappe_api import FrappeClient
def tools() -> list[Tool]:
return [
Tool(
name="frappe_aggregate_documents",
description=(
"Aggregate document data: sum, avg, min, max, or count grouped by a field. "
"e.g. total sales per customer, count of issues per status."
),
inputSchema={
"type": "object",
"required": ["doctype", "function", "field"],
"properties": {
"doctype": {"type": "string"},
"function": {
"type": "string",
"enum": ["sum", "avg", "min", "max", "count"],
},
"field": {"type": "string", "description": "Field to aggregate e.g. 'grand_total'"},
"group_by": {"type": "string", "description": "Field to group by e.g. 'customer'"},
"filters": {"type": "array", "items": {}},
"limit": {"type": "integer", "default": 50},
},
},
),
Tool(
name="frappe_get_dashboard_data",
description=(
"Pull data for a Dashboard Chart — returns the chart's data points "
"for display as a summary or KPI."
),
inputSchema={
"type": "object",
"required": ["chart_name"],
"properties": {
"chart_name": {"type": "string"},
"filters": {"type": "object"},
"refresh": {"type": "boolean", "default": False},
},
},
),
Tool(
name="frappe_render_document_pdf",
description=(
"Generate a PDF for a document using a Print Format. "
"Returns the PDF download URL on the Frappe server."
),
inputSchema={
"type": "object",
"required": ["doctype", "name"],
"properties": {
"doctype": {"type": "string"},
"name": {"type": "string"},
"print_format": {"type": "string", "description": "Print Format name (optional, uses default if empty)"},
"letterhead": {"type": "string"},
"language": {"type": "string", "default": "en"},
},
},
),
Tool(
name="frappe_get_number_card_value",
description="Get the current value of a Number Card (single KPI metric).",
inputSchema={
"type": "object",
"required": ["card_name"],
"properties": {
"card_name": {"type": "string"},
"filters": {"type": "object"},
},
},
),
]
def handlers() -> dict:
return {
"frappe_aggregate_documents": _aggregate_documents,
"frappe_get_dashboard_data": _get_dashboard_data,
"frappe_render_document_pdf": _render_document_pdf,
"frappe_get_number_card_value": _get_number_card_value,
}
async def _aggregate_documents(args: dict) -> str:
client = FrappeClient()
func = args["function"].upper()
field = args["field"]
group_by = args.get("group_by")
filters = args.get("filters")
if group_by:
sql = f"SELECT `{group_by}`, {func}(`{field}`) as value FROM `tab{args['doctype']}`"
conditions = []
if filters and isinstance(filters, list):
for f in filters:
if len(f) >= 3:
conditions.append(f"`{f[0]}` {f[1]} '{f[2]}'")
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" GROUP BY `{group_by}` ORDER BY value DESC LIMIT {args.get('limit', 50)}"
result = await client.call_method("frappe.client.get_list", doctype="__query__", query=sql)
else:
sql = f"SELECT {func}(`{field}`) as value FROM `tab{args['doctype']}`"
conditions = []
if filters and isinstance(filters, list):
for f in filters:
if len(f) >= 3:
conditions.append(f"`{f[0]}` {f[1]} '{f[2]}'")
if conditions:
sql += " WHERE " + " AND ".join(conditions)
result = await client.call_method(
"frappe.desk.query_report.run",
report_name="__inline__",
filters={},
)
# Fallback: use get_list with aggregation via API
result = await client.call_method(
"frappe.client.get_value",
doctype=args["doctype"],
filters=filters,
fieldname=f"{func.lower()}({field})",
)
return json.dumps({"function": func, "field": field, "group_by": group_by, "result": result}, indent=2)
async def _get_dashboard_data(args: dict) -> str:
client = FrappeClient()
result = await client.call_method(
"frappe.desk.doctype.dashboard_chart.dashboard_chart.get",
chart_name=args["chart_name"],
filters=json.dumps(args.get("filters", {})),
refresh=args.get("refresh", False),
)
return json.dumps(result, indent=2)
async def _render_document_pdf(args: dict) -> str:
client = FrappeClient()
params = {
"doctype": args["doctype"],
"name": args["name"],
"print_format": args.get("print_format", ""),
"letterhead": args.get("letterhead", ""),
"lang": args.get("language", "en"),
}
pdf_url = (
f"{client.base_url}/api/method/frappe.utils.pdf.download_pdf"
f"?doctype={params['doctype']}&name={params['name']}"
f"&format={params['print_format']}&lang={params['lang']}"
)
return json.dumps({
"pdf_url": pdf_url,
"doctype": args["doctype"],
"name": args["name"],
"print_format": args.get("print_format", "default"),
"note": "Open pdf_url in browser or download directly from Frappe server",
}, indent=2)
async def _get_number_card_value(args: dict) -> str:
client = FrappeClient()
result = await client.call_method(
"frappe.desk.doctype.number_card.number_card.get_result",
doc={"name": args["card_name"]},
filters=json.dumps(args.get("filters", {})),
)
return json.dumps({"card": args["card_name"], "value": result}, indent=2)