Skip to content

Commit e6010f8

Browse files
authored
feat: optimize hubspot tools (#393)
1 parent e070fa4 commit e6010f8

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

mcp_servers/hubspot/tools/deals.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@
66
# Configure logging
77
logger = logging.getLogger(__name__)
88

9+
def _build_dealstage_label_map(client) -> dict:
10+
"""
11+
Build a mapping from deal stage ID to its human-readable label across all deal pipelines.
12+
13+
Returns:
14+
- dict mapping stage_id -> label (e.g., {"appointmentscheduled": "Appointment Scheduled", "1890285259": "POC"})
15+
"""
16+
stage_id_to_label: dict = {}
17+
try:
18+
pipelines = client.crm.pipelines.pipelines_api.get_all("deals")
19+
for pipeline in getattr(pipelines, "results", []) or []:
20+
try:
21+
stages = client.crm.pipelines.pipeline_stages_api.get_all("deals", pipeline.id)
22+
for stage in getattr(stages, "results", []) or []:
23+
if getattr(stage, "id", None) and getattr(stage, "label", None):
24+
stage_id_to_label[stage.id] = stage.label
25+
except Exception as inner_exc:
26+
logger.debug(f"Failed to fetch stages for pipeline {getattr(pipeline, 'id', 'unknown')}: {inner_exc}")
27+
except Exception as exc:
28+
logger.debug(f"Failed to fetch pipelines for deals: {exc}")
29+
return stage_id_to_label
30+
931
async def hubspot_get_deals(limit: int = 10):
1032
"""
1133
Fetch a list of deals from HubSpot.
@@ -23,6 +45,14 @@ async def hubspot_get_deals(limit: int = 10):
2345
try:
2446
logger.info(f"Fetching up to {limit} deals...")
2547
result = client.crm.deals.basic_api.get_page(limit=limit)
48+
# Enrich with human-readable dealstage label
49+
stage_label_map = _build_dealstage_label_map(client)
50+
for obj in getattr(result, "results", []) or []:
51+
props = getattr(obj, "properties", {}) or {}
52+
stage_id = props.get("dealstage")
53+
if stage_id and stage_id in stage_label_map:
54+
props["dealstage_label"] = stage_label_map[stage_id]
55+
obj.properties = props
2656
logger.info(f"Fetched {len(result.results)} deals successfully.")
2757
return result
2858
except Exception as e:
@@ -46,6 +76,13 @@ async def hubspot_get_deal_by_id(deal_id: str):
4676
try:
4777
logger.info(f"Fetching deal ID: {deal_id}...")
4878
result = client.crm.deals.basic_api.get_by_id(deal_id)
79+
# Enrich with human-readable dealstage label
80+
stage_label_map = _build_dealstage_label_map(client)
81+
props = getattr(result, "properties", {}) or {}
82+
stage_id = props.get("dealstage")
83+
if stage_id and stage_id in stage_label_map:
84+
props["dealstage_label"] = stage_label_map[stage_id]
85+
result.properties = props
4986
logger.info(f"Fetched deal ID: {deal_id} successfully.")
5087
return result
5188
except Exception as e:

mcp_servers/hubspot/tools/properties.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
2+
import json
3+
import ast
24
from hubspot.crm.objects import Filter, FilterGroup, PublicObjectSearchRequest
35
from hubspot.crm.properties import PropertyCreate
46
from .base import get_hubspot_client
7+
from .deals import _build_dealstage_label_map
58

69
# Configure logging
710
logger = logging.getLogger(__name__)
@@ -121,10 +124,64 @@ async def hubspot_search_by_property(
121124
logger.info(f"Executing hubspot_search_by_property on {object_type}: {property_name} {operator} {value}")
122125

123126
try:
127+
# Build Filter with correct fields depending on operator
128+
filter_kwargs = {"property_name": property_name, "operator": operator}
129+
130+
# Operators that require no value
131+
if operator in {"HAS_PROPERTY", "NOT_HAS_PROPERTY"}:
132+
pass
133+
134+
# Operators that require a list of values
135+
elif operator in {"IN", "NOT_IN"}:
136+
values_list: list[str] = []
137+
try:
138+
parsed = json.loads(value)
139+
if isinstance(parsed, list):
140+
values_list = [str(v) for v in parsed]
141+
except Exception:
142+
try:
143+
parsed = ast.literal_eval(value)
144+
if isinstance(parsed, list):
145+
values_list = [str(v) for v in parsed]
146+
except Exception:
147+
# Fallback: split by comma
148+
values_list = [v.strip().strip('"\'') for v in value.split(',') if v.strip()]
149+
150+
if not values_list:
151+
raise ValueError("Operator IN/NOT_IN requires a non-empty list of values")
152+
153+
filter_kwargs["values"] = values_list
154+
155+
# Between expects two endpoints: low and high
156+
elif operator == "BETWEEN":
157+
low = None
158+
high = None
159+
try:
160+
parsed = json.loads(value)
161+
if isinstance(parsed, list) and len(parsed) >= 2:
162+
low, high = str(parsed[0]), str(parsed[1])
163+
except Exception:
164+
try:
165+
parsed = ast.literal_eval(value)
166+
if isinstance(parsed, list) and len(parsed) >= 2:
167+
low, high = str(parsed[0]), str(parsed[1])
168+
except Exception:
169+
pass
170+
171+
if low is None or high is None:
172+
raise ValueError("Operator BETWEEN requires a list with two values [low, high]")
173+
174+
filter_kwargs["value"] = low
175+
filter_kwargs["high_value"] = high
176+
177+
# All other operators use single value
178+
else:
179+
filter_kwargs["value"] = value
180+
124181
search_request = PublicObjectSearchRequest(
125182
filter_groups=[
126183
FilterGroup(filters=[
127-
Filter(property_name=property_name, operator=operator, value=value)
184+
Filter(**filter_kwargs)
128185
])
129186
],
130187
properties=list(properties),
@@ -143,6 +200,18 @@ async def hubspot_search_by_property(
143200
raise ValueError(f"Unsupported object type: {object_type}")
144201

145202
logger.info(f"hubspot_search_by_property: Found {len(results.results)} result(s)")
203+
# Enrich deals with human-readable dealstage label
204+
if object_type == "deals":
205+
stage_label_map = _build_dealstage_label_map(client)
206+
enriched: list[dict] = []
207+
for obj in results.results:
208+
props = (getattr(obj, "properties", {}) or {}).copy()
209+
stage_id = props.get("dealstage")
210+
if stage_id and stage_id in stage_label_map:
211+
props["dealstage_label"] = stage_label_map[stage_id]
212+
enriched.append(props)
213+
return enriched
214+
# For other objects, return properties as-is
146215
return [obj.properties for obj in results.results]
147216

148217
except Exception as e:

0 commit comments

Comments
 (0)