Skip to content

Commit a2af0e9

Browse files
authored
feat(search): add HubSpot CRM member search (#8387)
* feat(search): add HubSpot CRM member search * fix(search): bind HubSpot continuation and isolate setup
1 parent f00f5c5 commit a2af0e9

56 files changed

Lines changed: 1733 additions & 115 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/platform/connected-accounts.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ In **Providers**, select **Add provider** and search the catalog. Complete any r
3838
| OAuth providers, such as Gmail or Google Drive | Add the provider from the available catalog. Each invited person authorizes their account. |
3939
| Slack | Supply the Slack App ID, Slack workspace ID, OAuth client ID, and client secret, then complete app verification. The organization uses one app and Slack workspace. Existing workspace Slack bots remain separate. |
4040
| Fireflies and Granola | Add the provider. Sim supplies the MCP endpoint and handles OAuth client registration. People only complete their own authorization. |
41+
| HubSpot (member access) | The deployment supplies a registered HubSpot MCP OAuth app. People sign in and allow CRM read access. See [HubSpot Search](/search/hubspot). |
4142
| Databricks | Enter a name, the tenant's MCP URL, a registered OAuth client ID, and a client secret if required by that client. |
4243

4344
For Databricks, **Add** validates and saves the configuration before the provider appears in the list. Cancelling the form leaves nothing added. Use an official Databricks HTTPS MCP endpoint, such as `https://your-workspace.cloud.databricks.com/api/2.0/mcp/sql`, and the OAuth client registered for your deployment. People connecting later use this organization configuration.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: HubSpot
3+
description: Search contacts, companies, deals, and tickets with your HubSpot permissions
4+
---
5+
6+
HubSpot live Search uses **Member accounts** through HubSpot's official MCP service. An administrator enables HubSpot under **Settings → Sources**, then each person selects **Connect** under **Integrations** and signs in to HubSpot. Allow read access to contacts, companies, deals, and tickets. Write access is not needed.
7+
8+
## What you can search
9+
10+
Search names, domains, email addresses, deal names, and ticket subjects using concise keywords. Select a CRM kind to focus on contacts, companies, deals, or tickets; an unrestricted search checks all four. HubSpot searches each object's default searchable properties. It is lexical search, so a long natural-language question may need shorter keywords.
11+
12+
Read a result to see its returned CRM properties, including available custom properties. Links open the verified record in HubSpot. Reads do not include activity histories, notes, or associated records.
13+
14+
Dates and date sorting use the record's last modification time. To list records without keywords, select newest/oldest sorting or a date range. Continue an individual CRM kind with the returned cursor and the same query and filters. Searches are bounded and do not establish aggregate totals or revenue. Ownership, pipeline, lifecycle-stage filters, and custom object types are not supported; “my deals” is not treated as “all visible deals.”
15+
16+
## Access and setup
17+
18+
Every request uses the connected person's current HubSpot permissions. Search uses a fixed read-only tool allowlist and cannot create or change CRM records. Account permissions, subscription restrictions, and HubSpot request limits still apply.
19+
20+
The deployment configures one HubSpot MCP OAuth app; teammates only sign in. This is separate from the ordinary HubSpot OAuth app used by workflow integrations. For a self-hosted deployment, register an MCP app in HubSpot, set `HUBSPOT_MCP_CLIENT_ID` and `HUBSPOT_MCP_CLIENT_SECRET`, and register the exact callback `<your Sim app URL>/api/mcp/oauth/callback`. Sim uses `https://mcp.hubspot.com` and handles HubSpot's regional redirect. Replacing the shared registration requires people to reconnect.
21+
22+
If HubSpot is unavailable in the source picker, ask the deployment administrator to configure the MCP app. If read access is missing, reconnect with the needed read permissions. See [HubSpot's remote MCP server documentation](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server).

‎apps/docs/content/docs/search/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"google-calendar",
1414
"google-drive",
1515
"granola",
16+
"hubspot",
1617
"jira",
1718
"linear",
1819
"notion",

‎apps/sim/.env.example‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,8 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
261261
# SLACK_SEARCH_CLIENT_SECRET=
262262
# SLACK_SEARCH_SIGNING_SECRET=
263263
# SLACK_SEARCH_SHARED_APP=false # Off-production fallback for the global slack-search-shared-app flag
264+
265+
# HubSpot member search: deployment-owned MCP connector (separate from the REST OAuth app).
266+
# Register ${NEXT_PUBLIC_APP_URL}/api/mcp/oauth/callback in the HubSpot MCP connector.
267+
# HUBSPOT_MCP_CLIENT_ID=
268+
# HUBSPOT_MCP_CLIENT_SECRET=

‎apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,7 @@ describe('live integrations backend selection', () => {
998998
})
999999
mockUseOrganizationAccounts.mockReturnValue({
10001000
data: {
1001+
availableMcpConnectors: [],
10011002
credentialGroup: {
10021003
status: 'active',
10031004
mcpServers: [],
@@ -1031,6 +1032,7 @@ describe('live integrations backend selection', () => {
10311032
mocks.integrations.mockReturnValue({ data: [{ connectorType: 'slack', approved: true }] })
10321033
mockUseOrganizationAccounts.mockReturnValue({
10331034
data: {
1035+
availableMcpConnectors: [],
10341036
credentialGroup: {
10351037
status: 'active',
10361038
mcpServers: [],

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.test.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const group = {
6666
}
6767
const inventory = (overrides = {}) => ({
6868
credentialGroup: group,
69+
availableMcpConnectors: [],
6970
viewerAccounts: [],
7071
viewerMcpAccounts: [],
7172
canManage: false,

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
7575
const available = LIVE_SEARCH_SOURCE_TYPES.filter(
7676
([provider]) =>
7777
LIVE_SEARCH_SCOPE_FIELDS[provider] &&
78+
(provider !== 'hubspot' ||
79+
data.availableMcpConnectors.includes('hubspot') ||
80+
mcpAccounts(provider).length > 0) &&
7881
(approvals.get(provider)?.approved ||
7982
data.viewerAccounts?.some(
8083
(account) => liveSearchProviderForCredential(account.providerId) === provider
@@ -130,7 +133,8 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
130133
group?.status === 'active' &&
131134
Boolean(option || server) &&
132135
approved &&
133-
(!option || option.configurationStatus === 'ready')
136+
(!option || option.configurationStatus === 'ready') &&
137+
(provider !== 'hubspot' || data.availableMcpConnectors.includes('hubspot'))
134138
const scope =
135139
approval?.policy?.accessMode === 'service_account'
136140
? 'Selected resources you can access'

‎apps/sim/app/o/[organizationId]/settings/components/integrations/add-organization-source-modal.tsx‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface AddOrganizationSourceModalProps {
2727
type: string
2828
meta: Pick<ConnectorMeta, 'name' | 'icon'> & { auth?: ConnectorMeta['auth'] }
2929
access: { admin: boolean; members: boolean }
30+
availabilityStatus?: 'loading' | 'error'
3031
}[]
3132
pending: boolean
3233
ready: boolean
@@ -52,8 +53,9 @@ export function AddOrganizationSourceModal({
5253
const visible = sources.filter(({ meta }) => meta.name.toLowerCase().includes(query))
5354
const list = (
5455
<div className={RESOURCE_LIST_STACK}>
55-
{visible.map(({ type, meta, access }) => {
56+
{visible.map(({ type, meta, access, availabilityStatus }) => {
5657
const available = access.admin || access.members
58+
const sourceReady = ready && !availabilityStatus
5759
return (
5860
<SettingsResourceRow
5961
key={type}
@@ -67,26 +69,28 @@ export function AddOrganizationSourceModal({
6769
}
6870
title={meta.name}
6971
description={
70-
!ready
71-
? 'Checking availability'
72-
: !available
73-
? 'Unavailable in this deployment'
74-
: (descriptions?.[type] ??
75-
(access.admin
76-
? meta.auth?.mode === 'apiKey'
77-
? 'Connect an API token'
78-
: meta.auth?.mode === 'oauth' &&
79-
meta.auth.adminCredentialType === 'service_account'
80-
? 'Connect a service account'
81-
: 'Connect an admin account'
82-
: type === 'slack'
83-
? 'Set up your Slack app'
84-
: 'Connect member accounts'))
72+
availabilityStatus === 'error'
73+
? 'Could not check availability'
74+
: !sourceReady
75+
? 'Checking availability'
76+
: !available
77+
? 'Unavailable in this deployment'
78+
: (descriptions?.[type] ??
79+
(access.admin
80+
? meta.auth?.mode === 'apiKey'
81+
? 'Connect an API token'
82+
: meta.auth?.mode === 'oauth' &&
83+
meta.auth.adminCredentialType === 'service_account'
84+
? 'Connect a service account'
85+
: 'Connect an admin account'
86+
: type === 'slack'
87+
? 'Set up your Slack app'
88+
: 'Connect member accounts'))
8589
}
86-
disabled={pending || !ready || !available}
90+
disabled={pending || !sourceReady || !available}
8791
onClick={() => onSelect(type, access.admin ? 'admin' : 'members')}
8892
clickLabel={`Set up ${meta.name}`}
89-
navigable={ready && available}
93+
navigable={sourceReady && available}
9094
/>
9195
)
9296
})}

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-settings.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,15 @@ export function LiveSearchSettings() {
8686
).map(([type, meta]) => ({
8787
type,
8888
meta,
89+
availabilityStatus:
90+
type !== 'hubspot' || accounts.isSuccess
91+
? undefined
92+
: accounts.isError
93+
? ('error' as const)
94+
: ('loading' as const),
8995
access: getLiveSearchAccessAvailability(type, availability.integrationAvailability, {
9096
memberAccessAvailable: searchAccess.memberScoped,
97+
availableMcpConnectors: accounts.data?.availableMcpConnectors,
9198
mirroredAccessAvailable: searchAccess.sourceMirrored,
9299
oauthServiceAvailability: availability.oauthServiceAvailability,
93100
isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
@@ -301,7 +308,17 @@ export function LiveSearchSettings() {
301308
)}
302309
pending={update.isPending}
303310
ready={availability.isIntegrationAvailabilityReady}
304-
feedback={null}
311+
feedback={
312+
accounts.error ? (
313+
<SettingsQueryErrorState
314+
error={accounts.error}
315+
fallback='Could not check account availability'
316+
isRetrying={accounts.isFetching}
317+
onRetry={() => void accounts.refetch()}
318+
variant='inline'
319+
/>
320+
) : null
321+
}
305322
onClose={() => setAdding(false)}
306323
onSelect={(type, mode) => {
307324
if (type === GENERIC_SECRETS_SOURCE_TYPE) {

‎apps/sim/app/workspace/[workspaceId]/components/resource-tile/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export {
2+
BrandTile,
23
RESOURCE_TILE_BASE,
34
RESOURCE_TILE_FILL,
45
RESOURCE_TILE_PLAIN,

0 commit comments

Comments
 (0)