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
18 changes: 18 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ jobs:
if-no-files-found: ignore
retention-days: 7

- name: Verify Lucid MCP search and complete diagram reads over real HTTP
if: matrix.provision == 'push'
working-directory: apps/sim
env:
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3040
NEXT_PUBLIC_FORCE_HOSTED: 'false'
SEARCH_LUCID_REPORT_PATH: ${{ runner.temp }}/search-lucid.json
run: bun scripts/test-search-lucid-e2e.ts

- name: Upload Lucid acceptance report
if: failure() && matrix.provision == 'push'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: search-lucid
path: ${{ runner.temp }}/search-lucid.json
if-no-files-found: ignore
retention-days: 7

- name: Verify SCIM and administration over real HTTP
working-directory: apps/sim
env:
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import type { SVGProps } from 'react'
import { useId } from 'react'

interface LucidIconProps extends SVGProps<SVGSVGElement> {}

export function LucidIcon(props: LucidIconProps) {
return (
<svg
{...props}
viewBox='-22 0 144 144.5'
fill='currentColor'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M50 0L0 28.9V144.5L50 115.6V0Z' />
<path d='M100 142.7V86.7L50 115.6V142.7H100Z' />
</svg>
)
}

export function EnrichmentIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
24 changes: 24 additions & 0 deletions apps/docs/content/docs/search/lucid.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: Lucid
description: Search Lucidchart diagrams and Lucidspark boards with your own Lucid account
---

## Connect

An administrator enables **Lucid → Member accounts** in **Settings → Sources**. Each person then connects Lucid in **Integrations** and approves access with their own Lucid account.

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.

## Search

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.

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.

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.

## Diagram content

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.

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.
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 @@ -16,6 +16,7 @@
"hubspot",
"jira",
"linear",
"lucid",
"notion",
"slack"
]
Expand Down
16 changes: 16 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import type { SVGProps } from 'react'
import { useId } from 'react'

interface LucidIconProps extends SVGProps<SVGSVGElement> {}

export function LucidIcon(props: LucidIconProps) {
return (
<svg
{...props}
viewBox='-22 0 144 144.5'
fill='currentColor'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M50 0L0 28.9V144.5L50 115.6V0Z' />
<path d='M100 142.7V86.7L50 115.6V142.7H100Z' />
</svg>
)
}

export function EnrichmentIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/api/contracts/credential-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const createCredentialGroupMcpConnectorBodySchema = z.discriminatedUnion(
z.object({ connectorId: z.literal('notion') }).strict(),
z.object({ connectorId: z.literal('coda') }).strict(),
z.object({ connectorId: z.literal('hubspot') }).strict(),
z.object({ connectorId: z.literal('lucid') }).strict(),
z
.object({
connectorId: z.literal('databricks'),
Expand Down
16 changes: 10 additions & 6 deletions apps/sim/lib/api/contracts/mothership-assistant-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

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

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

function hasSearchKinds(
Expand All @@ -36,6 +39,7 @@ const nativeSearchKindSchema = z.enum([
...PROVIDER_KIND_SCHEMAS.github.options,
...PROVIDER_KIND_SCHEMAS.gitlab.options,
...PROVIDER_KIND_SCHEMAS.hubspot.options,
...PROVIDER_KIND_SCHEMAS.lucid.options,
])

/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
Expand All @@ -62,11 +66,11 @@ export const nativeSearchQuerySchema = z
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
})
}
if (input.provider === 'notion' && !input.query)
if ((input.provider === 'notion' || input.provider === 'lucid') && !input.query)
context.addIssue({
code: 'custom',
path: ['query'],
message: NOTION_SEARCH_TERMS_REQUIRED,
message: SEARCH_TERMS_REQUIRED[input.provider],
})
})
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>
Expand Down Expand Up @@ -106,7 +110,7 @@ export const nativeSearchQueriesSchema = z
earlier.some((previous) => !previous.kind || !query.kind)
)
addIssue(
'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.'
'A GitHub, GitLab, HubSpot, or Lucid query without a kind already searches its default kinds; give each query on this account a kind.'
)
else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
addIssue(
Expand Down Expand Up @@ -188,7 +192,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
`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.`
`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.`
Comment thread
waleedlatif1 marked this conversation as resolved.
),
query: z
.string()
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/credential-groups/managed-mcp-connector-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FirefliesIcon,
GranolaIcon,
HubspotIcon,
LucidIcon,
NotionIcon,
} from '@/components/icons'
import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
Expand All @@ -12,6 +13,7 @@ export const MANAGED_MCP_CONNECTOR_ICONS = {
fireflies: FirefliesIcon,
granola: GranolaIcon,
hubspot: HubspotIcon,
lucid: LucidIcon,
coda: CodaIcon,
notion: NotionIcon,
databricks: DatabricksIcon,
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/lib/credential-groups/managed-mcp-connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const MANAGED_MCP_CONNECTOR_IDS = [
'coda',
'notion',
'hubspot',
'lucid',
] as const

export type ManagedMcpConnectorId = (typeof MANAGED_MCP_CONNECTOR_IDS)[number]
Expand Down Expand Up @@ -38,6 +39,13 @@ export type ManagedMcpConnector =
| HubSpotManagedMcpConnector

export const MANAGED_MCP_CONNECTORS = {
lucid: {
id: 'lucid',
name: 'Lucid',
description: 'Search Lucidchart diagrams and Lucidspark boards using your Lucid account',
url: 'https://mcp.lucid.app/mcp/readonly',
oauthClientRegistration: 'dynamic',
},
hubspot: {
id: 'hubspot',
name: 'HubSpot',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ describe('atomic organization live Search MCP setup', () => {

beforeAll(() => {
vi.spyOn(dns, 'resolveHostAddresses').mockImplementation(async (hostname) => {
if (!['api.fireflies.ai', 'mcp.granola.ai', 'mcp.notion.com'].includes(hostname))
if (
!['api.fireflies.ai', 'mcp.granola.ai', 'mcp.notion.com', 'mcp.lucid.app'].includes(
hostname
)
)
throw new Error(`Unexpected DNS lookup in setup fixture: ${hostname}`)
return { addresses: ['93.184.216.34'], preferred: '93.184.216.34' }
})
Expand Down Expand Up @@ -105,6 +109,7 @@ describe('atomic organization live Search MCP setup', () => {
['fireflies', 'https://api.fireflies.ai/mcp'],
['granola', 'https://mcp.granola.ai/mcp'],
['notion', 'https://mcp.notion.com/mcp'],
['lucid', 'https://mcp.lucid.app/mcp/readonly'],
])(
'approves %s with an organization-owned sign-in server and access policy',
async (provider, url) => {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/mothership/generated/docs-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ export const DOCS_MANIFEST: readonly string[] = [
'search/hubspot.mdx',
'search/jira.mdx',
'search/linear.mdx',
'search/lucid.mdx',
'search/mcp.mdx',
'search/notion.mdx',
'search/slack.mdx',
Expand Down
17 changes: 11 additions & 6 deletions apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const liveSearchProviderSchema = z.enum([
'github',
'gitlab',
'linear',
'lucid',
'hubspot',
'fireflies',
'granola',
Expand All @@ -21,8 +22,10 @@ export const liveSearchProviderSchema = z.enum([
])
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>

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

/**
* Native queries one call may send to the same provider account. Alternatives run as separate
Expand All @@ -41,6 +44,7 @@ const PROVIDER_KIND_SCHEMAS = {
github: z.enum(['issues', 'code', 'repositories', 'commits']),
gitlab: z.enum(['issues', 'code', 'merge_requests', 'wiki']),
hubspot: z.enum(['contacts', 'companies', 'deals', 'tickets']),
lucid: z.enum(['lucidchart', 'lucidspark']),
} as const

function hasSearchKinds(
Expand All @@ -53,6 +57,7 @@ const nativeSearchKindSchema = z.enum([
...PROVIDER_KIND_SCHEMAS.github.options,
...PROVIDER_KIND_SCHEMAS.gitlab.options,
...PROVIDER_KIND_SCHEMAS.hubspot.options,
...PROVIDER_KIND_SCHEMAS.lucid.options,
])

/** Queries are data for fixed read-only provider endpoints, never URLs or credentials. */
Expand All @@ -79,11 +84,11 @@ export const nativeSearchQuerySchema = z
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
})
}
if (input.provider === 'notion' && !input.query)
if ((input.provider === 'notion' || input.provider === 'lucid') && !input.query)
context.addIssue({
code: 'custom',
path: ['query'],
message: NOTION_SEARCH_TERMS_REQUIRED,
message: SEARCH_TERMS_REQUIRED[input.provider],
})
})
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>
Expand Down Expand Up @@ -123,7 +128,7 @@ export const nativeSearchQueriesSchema = z
earlier.some((previous) => !previous.kind || !query.kind)
)
addIssue(
'A GitHub, GitLab, or HubSpot query without a kind already searches every kind; give each query on this account a kind.'
'A GitHub, GitLab, HubSpot, or Lucid query without a kind already searches its default kinds; give each query on this account a kind.'
)
else if (busiestAccountLoad(earlier) >= MAX_NATIVE_QUERIES_PER_ACCOUNT)
addIssue(
Expand Down Expand Up @@ -205,7 +210,7 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
nativeQueries: nativeSearchQueriesSchema
.optional()
.describe(
`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.`
`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.`
),
query: z
.string()
Expand Down
Loading
Loading