Skip to content

Commit df47953

Browse files
authored
feat(search): add Lucidchart and Lucidspark live search (#8475)
* feat(search): add Lucidchart and Lucidspark live search * fix(search): validate Lucid queries and isolate stale candidates
1 parent b8a0044 commit df47953

28 files changed

Lines changed: 1075 additions & 88 deletions

‎.github/workflows/test-build.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,24 @@ jobs:
147147
if-no-files-found: ignore
148148
retention-days: 7
149149

150+
- name: Verify Lucid MCP search and complete diagram reads over real HTTP
151+
if: matrix.provision == 'push'
152+
working-directory: apps/sim
153+
env:
154+
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3040
155+
NEXT_PUBLIC_FORCE_HOSTED: 'false'
156+
SEARCH_LUCID_REPORT_PATH: ${{ runner.temp }}/search-lucid.json
157+
run: bun scripts/test-search-lucid-e2e.ts
158+
159+
- name: Upload Lucid acceptance report
160+
if: failure() && matrix.provision == 'push'
161+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
162+
with:
163+
name: search-lucid
164+
path: ${{ runner.temp }}/search-lucid.json
165+
if-no-files-found: ignore
166+
retention-days: 7
167+
150168
- name: Verify SCIM and administration over real HTTP
151169
working-directory: apps/sim
152170
env:

‎apps/docs/components/icons.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type { SVGProps } from 'react'
22
import { useId } from 'react'
33

4+
interface LucidIconProps extends SVGProps<SVGSVGElement> {}
5+
6+
export function LucidIcon(props: LucidIconProps) {
7+
return (
8+
<svg
9+
{...props}
10+
viewBox='-22 0 144 144.5'
11+
fill='currentColor'
12+
xmlns='http://www.w3.org/2000/svg'
13+
>
14+
<path d='M50 0L0 28.9V144.5L50 115.6V0Z' />
15+
<path d='M100 142.7V86.7L50 115.6V142.7H100Z' />
16+
</svg>
17+
)
18+
}
19+
420
export function EnrichmentIcon(props: SVGProps<SVGSVGElement>) {
521
return (
622
<svg
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: Lucid
3+
description: Search Lucidchart diagrams and Lucidspark boards with your own Lucid account
4+
---
5+
6+
## Connect
7+
8+
An administrator enables **Lucid → Member accounts** in **Settings → Sources**. Each person then connects Lucid in **Integrations** and approves access with their own Lucid account.
9+
10+
Sim uses [Lucid’s official read-only MCP server](https://help.lucid.co/hc/en-us/articles/51290793390740-View-and-search-Lucid-content-with-AI-tools-using-the-Lucid-read-only-MCP-server), available on Free, Individual, Team and Enterprise plans; FedRAMP accounts are not supported. No developer app, API key or additional Sim environment variables are needed. Team and Enterprise administrators may need to enable MCP access in Lucid. Lucid excludes documents owned outside the connected account, even when shared with you.
11+
12+
## Search
13+
14+
Search requires terms, even when filtering by date. Results are ranked for relevance and may not match the title literally. Start with a short document-title query, such as `deployment architecture`. Select `lucidchart` or `lucidspark` to narrow the product, or search both. Read a result to inspect its diagram or board structure.
15+
16+
To find text inside a known document, use its UUID or Lucid URL as the native query’s `project` and enter a literal phrase such as `API Gateway`. This matches shape labels and sticky-note text, case-insensitively. It does not search notes, tags, links or comments.
17+
18+
Title queries allow up to 400 characters; document-scoped text queries allow 200. Boolean and field operators are unsupported. Document search has no continuation and verifies at most 10 candidates. Dates use modification time; filtering and sorting the returned candidates cannot establish the newest or oldest document across the entire account.
19+
20+
## Diagram content
21+
22+
Reads preserve Lucid’s structured pages, nodes, connections and properties, with links back to the source. Large responses can be read in successive windows of the same document version. Sim rejects incomplete or changed documents rather than treating a partial graph as complete.
23+
24+
A complete read is limited to 8 page regions and 512 KiB. Images and external links remain references; rendered diagrams, OCR, comments and Lucidscale are not included. See [Lucid’s MCP access rules](https://help.lucid.co/hc/en-us/articles/53780064823188-For-admins-Lucid-MCP-server-security-and-authorization) for account restrictions.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"hubspot",
1717
"jira",
1818
"linear",
19+
"lucid",
1920
"notion",
2021
"slack"
2122
]

‎apps/sim/components/icons.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type { SVGProps } from 'react'
22
import { useId } from 'react'
33

4+
interface LucidIconProps extends SVGProps<SVGSVGElement> {}
5+
6+
export function LucidIcon(props: LucidIconProps) {
7+
return (
8+
<svg
9+
{...props}
10+
viewBox='-22 0 144 144.5'
11+
fill='currentColor'
12+
xmlns='http://www.w3.org/2000/svg'
13+
>
14+
<path d='M50 0L0 28.9V144.5L50 115.6V0Z' />
15+
<path d='M100 142.7V86.7L50 115.6V142.7H100Z' />
16+
</svg>
17+
)
18+
}
19+
420
export function EnrichmentIcon(props: SVGProps<SVGSVGElement>) {
521
return (
622
<svg

‎apps/sim/lib/api/contracts/credential-groups.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ export const createCredentialGroupMcpConnectorBodySchema = z.discriminatedUnion(
372372
z.object({ connectorId: z.literal('notion') }).strict(),
373373
z.object({ connectorId: z.literal('coda') }).strict(),
374374
z.object({ connectorId: z.literal('hubspot') }).strict(),
375+
z.object({ connectorId: z.literal('lucid') }).strict(),
375376
z
376377
.object({
377378
connectorId: z.literal('databricks'),

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
44
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
55
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
66

7-
export const NOTION_SEARCH_TERMS_REQUIRED =
8-
'Notion requires search terms. Add keywords or a concise question.'
7+
export const SEARCH_TERMS_REQUIRED = {
8+
notion: 'Notion requires search terms. Add keywords or a concise question.',
9+
lucid: 'Lucid requires search terms. Add document-title keywords or a literal shape-text query.',
10+
} as const
911

1012
/**
1113
* Native queries one call may send to the same provider account. Alternatives run as separate
@@ -24,6 +26,7 @@ const PROVIDER_KIND_SCHEMAS = {
2426
github: z.enum(['issues', 'code', 'repositories', 'commits']),
2527
gitlab: z.enum(['issues', 'code', 'merge_requests', 'wiki']),
2628
hubspot: z.enum(['contacts', 'companies', 'deals', 'tickets']),
29+
lucid: z.enum(['lucidchart', 'lucidspark']),
2730
} as const
2831

2932
function hasSearchKinds(
@@ -36,6 +39,7 @@ const nativeSearchKindSchema = z.enum([
3639
...PROVIDER_KIND_SCHEMAS.github.options,
3740
...PROVIDER_KIND_SCHEMAS.gitlab.options,
3841
...PROVIDER_KIND_SCHEMAS.hubspot.options,
42+
...PROVIDER_KIND_SCHEMAS.lucid.options,
3943
])
4044

4145
/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
@@ -62,11 +66,11 @@ export const nativeSearchQuerySchema = z
6266
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
6367
})
6468
}
65-
if (input.provider === 'notion' && !input.query)
69+
if ((input.provider === 'notion' || input.provider === 'lucid') && !input.query)
6670
context.addIssue({
6771
code: 'custom',
6872
path: ['query'],
69-
message: NOTION_SEARCH_TERMS_REQUIRED,
73+
message: SEARCH_TERMS_REQUIRED[input.provider],
7074
})
7175
})
7276
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>
@@ -106,7 +110,7 @@ export const nativeSearchQueriesSchema = z
106110
earlier.some((previous) => !previous.kind || !query.kind)
107111
)
108112
addIssue(
109-
'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.'
113+
'A GitHub, GitLab, HubSpot, or Lucid query without a kind already searches its default kinds; give each query on this account a kind.'
110114
)
111115
else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
112116
addIssue(
@@ -188,7 +192,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
188192
nativeQueries: nativeSearchQueriesSchema
189193
.optional()
190194
.describe(
191-
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion always requires search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; ownership filters are unsupported. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
195+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies/HubSpot/Lucid terms, Granola natural-language questions, Notion keywords or AI questions when available). Blank queries require a date bound or sortBy newest/oldest; Notion and Lucid always require search terms. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; one GitHub, GitLab, or HubSpot query without a kind searches GitHub issues (plus code when the query has no date bound or boolean operators, as its status message says), GitLab issues, merge requests, and code, or every HubSpot CRM kind; other collections, and multiple queries on one account, each need a kind, which may repeat. HubSpot kinds are contacts, companies, deals, and tickets; Lucid kinds are lucidchart and lucidspark. Both reject ownership filters. Lucid searches titles with no search continuation; project can scope a literal shape-text query to one known document UUID or Lucid URL. Read for structured diagram evidence. Dates and sorting cover only retrieved candidates, not globally newest/oldest matches. Write queries from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
192196
),
193197
query: z
194198
.string()

‎apps/sim/lib/credential-groups/managed-mcp-connector-icons.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
FirefliesIcon,
55
GranolaIcon,
66
HubspotIcon,
7+
LucidIcon,
78
NotionIcon,
89
} from '@/components/icons'
910
import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
@@ -12,6 +13,7 @@ export const MANAGED_MCP_CONNECTOR_ICONS = {
1213
fireflies: FirefliesIcon,
1314
granola: GranolaIcon,
1415
hubspot: HubspotIcon,
16+
lucid: LucidIcon,
1517
coda: CodaIcon,
1618
notion: NotionIcon,
1719
databricks: DatabricksIcon,

‎apps/sim/lib/credential-groups/managed-mcp-connectors.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const MANAGED_MCP_CONNECTOR_IDS = [
55
'coda',
66
'notion',
77
'hubspot',
8+
'lucid',
89
] as const
910

1011
export type ManagedMcpConnectorId = (typeof MANAGED_MCP_CONNECTOR_IDS)[number]
@@ -38,6 +39,13 @@ export type ManagedMcpConnector =
3839
| HubSpotManagedMcpConnector
3940

4041
export const MANAGED_MCP_CONNECTORS = {
42+
lucid: {
43+
id: 'lucid',
44+
name: 'Lucid',
45+
description: 'Search Lucidchart diagrams and Lucidspark boards using your Lucid account',
46+
url: 'https://mcp.lucid.app/mcp/readonly',
47+
oauthClientRegistration: 'dynamic',
48+
},
4149
hubspot: {
4250
id: 'hubspot',
4351
name: 'HubSpot',

‎apps/sim/lib/knowledge/__integration__/search-mcp-setup.integration.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ describe('atomic organization live Search MCP setup', () => {
3030

3131
beforeAll(() => {
3232
vi.spyOn(dns, 'resolveHostAddresses').mockImplementation(async (hostname) => {
33-
if (!['api.fireflies.ai', 'mcp.granola.ai', 'mcp.notion.com'].includes(hostname))
33+
if (
34+
!['api.fireflies.ai', 'mcp.granola.ai', 'mcp.notion.com', 'mcp.lucid.app'].includes(
35+
hostname
36+
)
37+
)
3438
throw new Error(`Unexpected DNS lookup in setup fixture: ${hostname}`)
3539
return { addresses: ['93.184.216.34'], preferred: '93.184.216.34' }
3640
})
@@ -105,6 +109,7 @@ describe('atomic organization live Search MCP setup', () => {
105109
['fireflies', 'https://api.fireflies.ai/mcp'],
106110
['granola', 'https://mcp.granola.ai/mcp'],
107111
['notion', 'https://mcp.notion.com/mcp'],
112+
['lucid', 'https://mcp.lucid.app/mcp/readonly'],
108113
])(
109114
'approves %s with an organization-owned sign-in server and access policy',
110115
async (provider, url) => {

0 commit comments

Comments
 (0)