diff --git a/server.py b/server.py index 9cbdd25..910be5c 100644 --- a/server.py +++ b/server.py @@ -85,6 +85,19 @@ def _fmt(data: Any) -> str: return str(data) +# Filter params shared by list_deals and deal_statistics, so the same filter set +# is combinable with any listing or grouping. +_DEAL_STR_FILTERS = ( + "firm", "stage", "status", "deal_type", "industry", "industry_category", + "country", "target_industry", "target_country", + "buyer_industry", "buyer_country", "seller_industry", "seller_country", + "pending_rfh", + "creation_from", "creation_to", "closing_from", "closing_to", + "updated_from", "updated_to", +) +_DEAL_INT_FILTERS = ("year_from", "year_to", "closing_year_from", "closing_year_to") + + # --------------------------------------------------------------------------- # MCP Server # --------------------------------------------------------------------------- @@ -139,23 +152,84 @@ async def list_tools() -> list[types.Tool]: }, "industry": { "type": "string", - "description": "Filter by industry sector (partial match on Target/Seller/Buyer). E.g. 'Automotive', 'Tech'.", + "description": "Filter by industry (partial match on Target/Seller/Buyer industry name). E.g. 'Automotive', 'Tech'.", + }, + "industry_category": { + "type": "string", + "description": "Filter by Industry Category — the sector level above Industry (partial match on Target/Seller/Buyer). E.g. 'Industrials', 'Consumer'.", }, "country": { "type": "string", "description": "Filter by country (partial match on Target/Seller/Buyer country). E.g. 'Italy', 'Germany'.", }, + "firm": { + "type": "string", + "description": "Filter by advisor firm name (partial match). E.g. 'Scouting', 'Quore'.", + }, + "buyer_industry": { + "type": "string", + "description": "Filter specifically by the Buyer's industry (partial match).", + }, + "buyer_country": { + "type": "string", + "description": "Filter specifically by the Buyer's country (partial match).", + }, + "seller_industry": { + "type": "string", + "description": "Filter specifically by the Seller's industry (partial match).", + }, + "seller_country": { + "type": "string", + "description": "Filter specifically by the Seller's country (partial match).", + }, + "pending_rfh": { + "type": "string", + "enum": ["pending", "closed", "any"], + "description": "Filter by Request For Help state: 'pending' = open RFH, 'closed' = closed RFH, 'any' = has any RFH (open or closed).", + }, "search": { "type": "string", "description": "Free-text search across TargetName, BuyerName, SellerName, MandateDescription.", }, "year_from": { "type": "integer", - "description": "Filter deals created from this year onwards.", + "description": "Filter deals whose CREATION year is >= this.", }, "year_to": { "type": "integer", - "description": "Filter deals created up to this year.", + "description": "Filter deals whose CREATION year is <= this.", + }, + "closing_year_from": { + "type": "integer", + "description": "Filter deals whose CLOSING year is >= this.", + }, + "closing_year_to": { + "type": "integer", + "description": "Filter deals whose CLOSING year is <= this.", + }, + "creation_from": { + "type": "string", + "description": "Creation date range start (YYYY-MM-DD).", + }, + "creation_to": { + "type": "string", + "description": "Creation date range end (YYYY-MM-DD).", + }, + "closing_from": { + "type": "string", + "description": "Closing date range start (YYYY-MM-DD).", + }, + "closing_to": { + "type": "string", + "description": "Closing date range end (YYYY-MM-DD).", + }, + "updated_from": { + "type": "string", + "description": "Last-update date range start (YYYY-MM-DD).", + }, + "updated_to": { + "type": "string", + "description": "Last-update date range end (YYYY-MM-DD).", }, "shared_only": { "type": "boolean", @@ -198,29 +272,77 @@ async def list_tools() -> list[types.Tool]: types.Tool( name="deal_statistics", description=( - "Returns aggregated statistics on visible deals, grouped by a chosen dimension. " - "Useful for analytics questions like:\n" - "- 'How many deals did we close per year?'\n" - "- 'Which industry has the most deals?'\n" - "- 'What is our deal breakdown by stage?'" + "Returns aggregated counts (and non-confidential total deal value) on visible " + "deals, grouped by one or two dimensions, with the same filters as list_deals so " + "any filter is combinable with any grouping.\n\n" + "Single dimension → rows of {label, count, total_value}.\n" + "Two dimensions (group_by_2) → cross-tab in long format: rows of " + "{label, label2, count, total_value} — ideal to then pivot (e.g. year × firm).\n\n" + "Useful for questions like:\n" + "- 'How many deals did we close per year?' (group_by='year_closed')\n" + "- 'Deals by firm and year' (group_by='year', group_by_2='firm')\n" + "- 'Crossborder vs domestic by industry category' (group_by='crossborder', group_by_2='industry_category')\n" + "- 'Buy-side deals by seller country in 2024' (group_by='seller_country', deal_type='Buy', year_from=2024, year_to=2024)" ), inputSchema={ "type": "object", "properties": { "group_by": { "type": "string", - "enum": ["stage", "industry", "deal_type", "year", "month", "firm", "country"], - "description": "Dimension to group by. 'month' = per month (YYYY-MM). 'country' = target country. Default: 'stage'.", + "enum": [ + "stage", "industry", "industry_category", "deal_type", + "year", "year_closed", "month", "firm", "country", + "target_industry", "target_country", + "buyer_industry", "buyer_country", + "seller_industry", "seller_country", "crossborder", + ], + "description": ( + "Primary dimension. 'year'/'month' = by creation date; 'year_closed' = by closing date; " + "'country' = target country; 'industry_category' = sector above industry. Default: 'stage'." + ), "default": "stage", }, - "year_from": { - "type": "integer", - "description": "Include deals from this year onwards.", + "group_by_2": { + "type": "string", + "enum": [ + "stage", "industry", "industry_category", "deal_type", + "year", "year_closed", "month", "firm", "country", + "target_industry", "target_country", + "buyer_industry", "buyer_country", + "seller_industry", "seller_country", "crossborder", + ], + "description": "Optional second dimension for a cross-tab (e.g. group_by='year', group_by_2='firm').", }, - "year_to": { - "type": "integer", - "description": "Include deals up to this year.", + "firm": {"type": "string", "description": "Filter by advisor firm name (partial match)."}, + "stage": {"type": "string", "description": "Filter by deal stage (partial match)."}, + "status": { + "type": "string", + "enum": ["open", "closed", "all"], + "description": "'open' = not Closed/Lost; 'closed' = Closed/Lost.", }, + "deal_type": {"type": "string", "description": "Filter by deal type (partial match). E.g. 'Sell', 'Buy'."}, + "industry": {"type": "string", "description": "Filter by industry name (Target/Seller/Buyer, partial match)."}, + "industry_category": {"type": "string", "description": "Filter by Industry Category / sector (Target/Seller/Buyer, partial match)."}, + "country": {"type": "string", "description": "Filter by country (Target/Seller/Buyer, partial match)."}, + "buyer_industry": {"type": "string", "description": "Filter by the Buyer's industry (partial match)."}, + "buyer_country": {"type": "string", "description": "Filter by the Buyer's country (partial match)."}, + "seller_industry": {"type": "string", "description": "Filter by the Seller's industry (partial match)."}, + "seller_country": {"type": "string", "description": "Filter by the Seller's country (partial match)."}, + "pending_rfh": { + "type": "string", + "enum": ["pending", "closed", "any"], + "description": "Filter by Request For Help state.", + }, + "year_from": {"type": "integer", "description": "Creation year >= this."}, + "year_to": {"type": "integer", "description": "Creation year <= this."}, + "closing_year_from": {"type": "integer", "description": "Closing year >= this."}, + "closing_year_to": {"type": "integer", "description": "Closing year <= this."}, + "creation_from": {"type": "string", "description": "Creation date range start (YYYY-MM-DD)."}, + "creation_to": {"type": "string", "description": "Creation date range end (YYYY-MM-DD)."}, + "closing_from": {"type": "string", "description": "Closing date range start (YYYY-MM-DD)."}, + "closing_to": {"type": "string", "description": "Closing date range end (YYYY-MM-DD)."}, + "updated_from": {"type": "string", "description": "Last-update date range start (YYYY-MM-DD)."}, + "updated_to": {"type": "string", "description": "Last-update date range end (YYYY-MM-DD)."}, }, }, ), @@ -475,7 +597,10 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: if "firm" in meta: summary_parts.append(f"Firm: {meta['firm']}") if "group_by" in meta: - summary_parts.append(f"Grouped by: {meta['group_by']}") + gb = meta["group_by"] + if meta.get("group_by_2"): + gb += " × " + meta["group_by_2"] + summary_parts.append(f"Grouped by: {gb}") if "unanswered_only" in meta: summary_parts.append(f"Unanswered only: {meta['unanswered_only']}") if summary_parts: @@ -492,10 +617,10 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: # ------------------------------------------------------------------ elif name == "list_deals": params: dict[str, Any] = {} - for key in ("deal_type", "stage", "status", "industry", "country", "search"): + for key in _DEAL_STR_FILTERS + ("search",): if arguments.get(key): params[key] = arguments[key] - for key in ("year_from", "year_to", "limit", "offset"): + for key in _DEAL_INT_FILTERS + ("limit", "offset"): if arguments.get(key) is not None: params[key] = int(arguments[key]) if arguments.get("shared_only"): @@ -511,13 +636,16 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: # ------------------------------------------------------------------ elif name == "deal_statistics": - params = {} - if arguments.get("group_by"): - params["group_by"] = arguments["group_by"] - if arguments.get("year_from"): - params["year_from"] = int(arguments["year_from"]) - if arguments.get("year_to"): - params["year_to"] = int(arguments["year_to"]) + params: dict[str, Any] = {} + for key in ("group_by", "group_by_2"): + if arguments.get(key): + params[key] = arguments[key] + for key in _DEAL_STR_FILTERS: + if arguments.get(key): + params[key] = arguments[key] + for key in _DEAL_INT_FILTERS: + if arguments.get(key) is not None: + params[key] = int(arguments[key]) result = await _get("/stats", params) return await respond(result)