Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/content/docs/platform/connected-accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ In **Providers**, select **Add provider** and search the catalog. Complete any r
| OAuth providers, such as Gmail or Google Drive | Add the provider from the available catalog. Each invited person authorizes their account. |
| 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. |
| Fireflies and Granola | Add the provider. Sim supplies the MCP endpoint and handles OAuth client registration. People only complete their own authorization. |
| 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). |
| Databricks | Enter a name, the tenant's MCP URL, a registered OAuth client ID, and a client secret if required by that client. |

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.
Expand Down
22 changes: 22 additions & 0 deletions apps/docs/content/docs/search/hubspot.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: HubSpot
description: Search contacts, companies, deals, and tickets with your HubSpot permissions
---

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.

## What you can search

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.

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.

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.”

## Access and setup

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.

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.

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).
1 change: 1 addition & 0 deletions apps/docs/content/docs/search/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"google-calendar",
"google-drive",
"granola",
"hubspot",
"jira",
"linear",
"notion",
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,8 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
# SLACK_SEARCH_CLIENT_SECRET=
# SLACK_SEARCH_SIGNING_SECRET=
# SLACK_SEARCH_SHARED_APP=false # Off-production fallback for the global slack-search-shared-app flag

# HubSpot member search: deployment-owned MCP connector (separate from the REST OAuth app).
# Register ${NEXT_PUBLIC_APP_URL}/api/mcp/oauth/callback in the HubSpot MCP connector.
# HUBSPOT_MCP_CLIENT_ID=
# HUBSPOT_MCP_CLIENT_SECRET=
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ describe('live integrations backend selection', () => {
})
mockUseOrganizationAccounts.mockReturnValue({
data: {
availableMcpConnectors: [],
credentialGroup: {
status: 'active',
mcpServers: [],
Expand Down Expand Up @@ -1031,6 +1032,7 @@ describe('live integrations backend selection', () => {
mocks.integrations.mockReturnValue({ data: [{ connectorType: 'slack', approved: true }] })
mockUseOrganizationAccounts.mockReturnValue({
data: {
availableMcpConnectors: [],
credentialGroup: {
status: 'active',
mcpServers: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const group = {
}
const inventory = (overrides = {}) => ({
credentialGroup: group,
availableMcpConnectors: [],
viewerAccounts: [],
viewerMcpAccounts: [],
canManage: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
const available = LIVE_SEARCH_SOURCE_TYPES.filter(
([provider]) =>
LIVE_SEARCH_SCOPE_FIELDS[provider] &&
(provider !== 'hubspot' ||
data.availableMcpConnectors.includes('hubspot') ||
mcpAccounts(provider).length > 0) &&
(approvals.get(provider)?.approved ||
data.viewerAccounts?.some(
(account) => liveSearchProviderForCredential(account.providerId) === provider
Expand Down Expand Up @@ -130,7 +133,8 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
group?.status === 'active' &&
Boolean(option || server) &&
approved &&
(!option || option.configurationStatus === 'ready')
(!option || option.configurationStatus === 'ready') &&
(provider !== 'hubspot' || data.availableMcpConnectors.includes('hubspot'))
Comment thread
waleedlatif1 marked this conversation as resolved.
const scope =
approval?.policy?.accessMode === 'service_account'
? 'Selected resources you can access'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface AddOrganizationSourceModalProps {
type: string
meta: Pick<ConnectorMeta, 'name' | 'icon'> & { auth?: ConnectorMeta['auth'] }
access: { admin: boolean; members: boolean }
availabilityStatus?: 'loading' | 'error'
}[]
pending: boolean
ready: boolean
Expand All @@ -52,8 +53,9 @@ export function AddOrganizationSourceModal({
const visible = sources.filter(({ meta }) => meta.name.toLowerCase().includes(query))
const list = (
<div className={RESOURCE_LIST_STACK}>
{visible.map(({ type, meta, access }) => {
{visible.map(({ type, meta, access, availabilityStatus }) => {
const available = access.admin || access.members
const sourceReady = ready && !availabilityStatus
return (
<SettingsResourceRow
key={type}
Expand All @@ -67,26 +69,28 @@ export function AddOrganizationSourceModal({
}
title={meta.name}
description={
!ready
? 'Checking availability'
: !available
? 'Unavailable in this deployment'
: (descriptions?.[type] ??
(access.admin
? meta.auth?.mode === 'apiKey'
? 'Connect an API token'
: meta.auth?.mode === 'oauth' &&
meta.auth.adminCredentialType === 'service_account'
? 'Connect a service account'
: 'Connect an admin account'
: type === 'slack'
? 'Set up your Slack app'
: 'Connect member accounts'))
availabilityStatus === 'error'
? 'Could not check availability'
: !sourceReady
? 'Checking availability'
: !available
? 'Unavailable in this deployment'
: (descriptions?.[type] ??
(access.admin
? meta.auth?.mode === 'apiKey'
? 'Connect an API token'
: meta.auth?.mode === 'oauth' &&
meta.auth.adminCredentialType === 'service_account'
? 'Connect a service account'
: 'Connect an admin account'
: type === 'slack'
? 'Set up your Slack app'
: 'Connect member accounts'))
}
disabled={pending || !ready || !available}
disabled={pending || !sourceReady || !available}
onClick={() => onSelect(type, access.admin ? 'admin' : 'members')}
clickLabel={`Set up ${meta.name}`}
navigable={ready && available}
navigable={sourceReady && available}
/>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ export function LiveSearchSettings() {
).map(([type, meta]) => ({
type,
meta,
availabilityStatus:
type !== 'hubspot' || accounts.isSuccess
? undefined
: accounts.isError
? ('error' as const)
: ('loading' as const),
access: getLiveSearchAccessAvailability(type, availability.integrationAvailability, {
memberAccessAvailable: searchAccess.memberScoped,
availableMcpConnectors: accounts.data?.availableMcpConnectors,
mirroredAccessAvailable: searchAccess.sourceMirrored,
oauthServiceAvailability: availability.oauthServiceAvailability,
isIntegrationAvailabilityReady: availability.isIntegrationAvailabilityReady,
Expand Down Expand Up @@ -301,7 +308,17 @@ export function LiveSearchSettings() {
)}
pending={update.isPending}
ready={availability.isIntegrationAvailabilityReady}
feedback={null}
feedback={
accounts.error ? (
<SettingsQueryErrorState
error={accounts.error}
fallback='Could not check account availability'
isRetrying={accounts.isFetching}
onRetry={() => void accounts.refetch()}
variant='inline'
/>
) : null
}
onClose={() => setAdding(false)}
onSelect={(type, mode) => {
if (type === GENERIC_SECRETS_SOURCE_TYPE) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
BrandTile,
RESOURCE_TILE_BASE,
RESOURCE_TILE_FILL,
RESOURCE_TILE_PLAIN,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentType } from 'react'
import { cn } from '@sim/emcn'
import { getTileIconColorClass } from '@/blocks/icon-color'

interface ResourceTileProps {
icon: ComponentType<{ className?: string }>
Expand Down Expand Up @@ -31,3 +32,23 @@ export function ResourceTile({ icon: Icon }: ResourceTileProps) {
</div>
)
}

interface BrandTileProps {
icon: ComponentType<{ className?: string }>
background: string | null | undefined
}

/** Shared brand treatment for integration and credential rows. */
export function BrandTile({ icon: Icon, background }: BrandTileProps) {
return (
<div
aria-hidden
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
style={background ? { background } : undefined}
>
<Icon
className={background ? getTileIconColorClass(background) : 'text-[var(--text-icon)]'}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ComponentType } from 'react'
import { cn } from '@sim/emcn'
import {
RESOURCE_TILE_BASE,
RESOURCE_TILE_PLAIN,
} from '@/app/workspace/[workspaceId]/components/resource-tile'
import { BrandTile } from '@/app/workspace/[workspaceId]/components/resource-tile'
import { getBlock } from '@/blocks'
import { getTileIconColorClass } from '@/blocks/icon-color'

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

if (!framed) {
return (
<div
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
style={brandBg ? { background: brandBg } : undefined}
>
<Icon className={getTileIconColorClass(brandBg)} />
</div>
)
}
if (!framed) return <BrandTile icon={Icon} background={brandBg} />

return (
<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)]'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { CredentialGroupProviderTile } from '@/ee/credential-groups/components/credential-group-provider-tile'
import { SlackManagedUsersModal } from '@/ee/credential-groups/components/slack-managed-users-modal'
import {
useCreateCredentialGroupMcpConnector,
Expand Down Expand Up @@ -199,6 +200,11 @@ export function CredentialGroupDetails({
return getCredentialGroupProviderService(provider).name.toLowerCase().includes(providerQuery)
})
const shownMcpConnectors = MANAGED_MCP_CONNECTOR_IDS.filter((connectorId) => {
if (
!credentialGroup.mcpServers.some((server) => server.managedConnectorId === connectorId) &&
!accounts.data?.availableMcpConnectors.includes(connectorId)
)
return false
if (!providerQuery) return true
const connector = MANAGED_MCP_CONNECTORS[connectorId]
return (
Expand Down Expand Up @@ -245,7 +251,8 @@ export function CredentialGroupDetails({
return (
<SettingsResourceRow
key={provider}
icon={<ProviderIcon aria-hidden />}
iconVariant='custom'
icon={<CredentialGroupProviderTile provider={provider} icon={ProviderIcon} />}
title={service.name}
description={descriptionText}
badge={
Expand Down Expand Up @@ -318,7 +325,8 @@ export function CredentialGroupDetails({
return (
<SettingsResourceRow
key={connectorId}
icon={<ConnectorIcon aria-hidden />}
iconVariant='custom'
icon={<CredentialGroupProviderTile provider={connectorId} icon={ConnectorIcon} />}
title={server?.name ?? connector.name}
description={
connectorId === 'databricks'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ComponentType } from 'react'
import { getIntegrationTypesForOAuthServiceId } from '@sim/deployment-config/integration-availability'
import { INTEGRATION_METADATA } from '@sim/deployment-config/integration-metadata'
import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
import { blockTypeToIconMap } from '@/lib/integrations/icon-mapping'
import { BrandTile } from '@/app/workspace/[workspaceId]/components/resource-tile'

const INTEGRATION_BY_TYPE = new Map(INTEGRATION_METADATA.map((entry) => [entry.type, entry]))

interface CredentialGroupProviderTileProps {
provider: CredentialGroupProvider | ManagedMcpConnectorId
icon: ComponentType<{ className?: string }>
}

export function CredentialGroupProviderTile({ provider, icon }: CredentialGroupProviderTileProps) {
const blockType = getIntegrationTypesForOAuthServiceId(provider)[0] ?? provider
return (
<BrandTile
icon={blockTypeToIconMap[blockType] ?? icon}
background={INTEGRATION_BY_TYPE.get(blockType)?.bgColor}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
RESOURCE_LIST_STACK,
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { CredentialGroupProviderTile } from '@/ee/credential-groups/components/credential-group-provider-tile'

export type OrganizationAccountProviderChoice =
| { kind: 'oauth'; provider: CredentialGroupProvider }
Expand Down Expand Up @@ -68,7 +69,10 @@ export function OrganizationAccountProviderCatalog({
)
)
.map((connectorId) => ({
name: MANAGED_MCP_CONNECTORS[connectorId].name,
name:
connectorId === 'hubspot'
? 'HubSpot (member access)'
: MANAGED_MCP_CONNECTORS[connectorId].name,
icon: getManagedMcpConnectorIcon(connectorId),
choice: { kind: 'mcp', connectorId } as const,
})),
Expand Down Expand Up @@ -108,8 +112,18 @@ export function OrganizationAccountProviderCatalog({
<div className={RESOURCE_LIST_STACK}>
{providers.map(({ name, icon: Icon, choice }) => (
<SettingsResourceRow
key={choice.kind === 'oauth' ? choice.provider : choice.connectorId}
icon={<Icon aria-hidden />}
key={
choice.kind === 'oauth'
? `oauth:${choice.provider}`
: `mcp:${choice.connectorId}`
}
iconVariant='custom'
icon={
<CredentialGroupProviderTile
provider={choice.kind === 'oauth' ? choice.provider : choice.connectorId}
icon={Icon}
/>
}
title={name}
trailing={
<Chip
Expand Down
Loading
Loading