Skip to content

Commit 4841138

Browse files
committed
feat(search): add HubSpot CRM member search
1 parent 35efc8a commit 4841138

51 files changed

Lines changed: 1410 additions & 73 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/live-member-integrations.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
130130
group?.status === 'active' &&
131131
Boolean(option || server) &&
132132
approved &&
133-
(!option || option.configurationStatus === 'ready')
133+
(!option || option.configurationStatus === 'ready') &&
134+
(provider !== 'hubspot' || data.availableMcpConnectors.includes('hubspot'))
134135
const scope =
135136
approval?.policy?.accessMode === 'service_account'
136137
? 'Selected resources you can access'

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export function LiveSearchSettings() {
8888
meta,
8989
access: getLiveSearchAccessAvailability(type, availability.integrationAvailability, {
9090
memberAccessAvailable: searchAccess.memberScoped,
91+
availableMcpConnectors: accounts.data?.availableMcpConnectors,
9192
mirroredAccessAvailable: searchAccess.sourceMirrored,
9293
oauthServiceAvailability: availability.oauthServiceAvailability,
9394
isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
@@ -300,8 +301,18 @@ export function LiveSearchSettings() {
300301
])
301302
)}
302303
pending={update.isPending}
303-
ready={availability.isIntegrationAvailabilityReady}
304-
feedback={null}
304+
ready={availability.isIntegrationAvailabilityReady && accounts.isSuccess}
305+
feedback={
306+
accounts.error ? (
307+
<SettingsQueryErrorState
308+
error={accounts.error}
309+
fallback='Could not check account availability'
310+
isRetrying={accounts.isFetching}
311+
onRetry={() => void accounts.refetch()}
312+
variant='inline'
313+
/>
314+
) : null
315+
}
305316
onClose={() => setAdding(false)}
306317
onSelect={(type, mode) => {
307318
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,

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentType } from 'react'
22
import { cn } from '@sim/emcn'
3+
import { getTileIconColorClass } from '@/blocks/icon-color'
34

45
interface ResourceTileProps {
56
icon: ComponentType<{ className?: string }>
@@ -31,3 +32,21 @@ export function ResourceTile({ icon: Icon }: ResourceTileProps) {
3132
</div>
3233
)
3334
}
35+
36+
interface BrandTileProps {
37+
icon: ComponentType<{ className?: string }>
38+
background: string | null | undefined
39+
}
40+
41+
/** Shared brand treatment for integration and credential rows. */
42+
export function BrandTile({ icon: Icon, background }: BrandTileProps) {
43+
return (
44+
<div
45+
aria-hidden
46+
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
47+
style={background ? { background } : undefined}
48+
>
49+
<Icon className={getTileIconColorClass(background)} />
50+
</div>
51+
)
52+
}

‎apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type { ComponentType } from 'react'
22
import { cn } from '@sim/emcn'
3-
import {
4-
RESOURCE_TILE_BASE,
5-
RESOURCE_TILE_PLAIN,
6-
} from '@/app/workspace/[workspaceId]/components/resource-tile'
3+
import { BrandTile } from '@/app/workspace/[workspaceId]/components/resource-tile'
74
import { getBlock } from '@/blocks'
85
import { getTileIconColorClass } from '@/blocks/icon-color'
96

@@ -71,16 +68,7 @@ interface IntegrationTileProps {
7168
export function IntegrationTile({ blockType, icon: Icon, framed = false }: IntegrationTileProps) {
7269
const brandBg = resolveBrandTileBg(blockType)
7370

74-
if (!framed) {
75-
return (
76-
<div
77-
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
78-
style={brandBg ? { background: brandBg } : undefined}
79-
>
80-
<Icon className={getTileIconColorClass(brandBg)} />
81-
</div>
82-
)
83-
}
71+
if (!framed) return <BrandTile icon={Icon} background={brandBg} />
8472

8573
return (
8674
<div className='size-11 shrink-0 rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-xs dark:bg-[var(--surface-5)]'>

‎apps/sim/ee/credential-groups/components/credential-group-details.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
SettingsResourceRow,
3333
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
3434
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
35+
import { CredentialGroupProviderTile } from '@/ee/credential-groups/components/credential-group-provider-tile'
3536
import { SlackManagedUsersModal } from '@/ee/credential-groups/components/slack-managed-users-modal'
3637
import {
3738
useCreateCredentialGroupMcpConnector,
@@ -199,6 +200,11 @@ export function CredentialGroupDetails({
199200
return getCredentialGroupProviderService(provider).name.toLowerCase().includes(providerQuery)
200201
})
201202
const shownMcpConnectors = MANAGED_MCP_CONNECTOR_IDS.filter((connectorId) => {
203+
if (
204+
!credentialGroup.mcpServers.some((server) => server.managedConnectorId === connectorId) &&
205+
!accounts.data?.availableMcpConnectors.includes(connectorId)
206+
)
207+
return false
202208
if (!providerQuery) return true
203209
const connector = MANAGED_MCP_CONNECTORS[connectorId]
204210
return (
@@ -245,7 +251,8 @@ export function CredentialGroupDetails({
245251
return (
246252
<SettingsResourceRow
247253
key={provider}
248-
icon={<ProviderIcon aria-hidden />}
254+
iconVariant='custom'
255+
icon={<CredentialGroupProviderTile provider={provider} icon={ProviderIcon} />}
249256
title={service.name}
250257
description={descriptionText}
251258
badge={
@@ -318,7 +325,8 @@ export function CredentialGroupDetails({
318325
return (
319326
<SettingsResourceRow
320327
key={connectorId}
321-
icon={<ConnectorIcon aria-hidden />}
328+
iconVariant='custom'
329+
icon={<CredentialGroupProviderTile provider={connectorId} icon={ConnectorIcon} />}
322330
title={server?.name ?? connector.name}
323331
description={
324332
connectorId === 'databricks'

0 commit comments

Comments
 (0)