Skip to content

Commit b5b2d83

Browse files
authored
v0.6.89: connectors ui, perf improvements, mcp hardening, og image
2 parents e9ee351 + a14d374 commit b5b2d83

55 files changed

Lines changed: 20316 additions & 576 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.

.github/workflows/migrations.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
working-directory: ./packages/db
4040
env:
4141
DATABASE_URL: ${{ github.ref == 'refs/heads/main' && secrets.DATABASE_URL || github.ref == 'refs/heads/dev' && secrets.DEV_DATABASE_URL || secrets.STAGING_DATABASE_URL }}
42-
run: bunx drizzle-kit migrate --config=./drizzle.config.ts
42+
run: bun run ./scripts/migrate.ts

apps/sim/app/(landing)/components/collaboration/collaboration.tsx

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useRef, useState } from 'react'
3+
import { useCallback, useId, useRef, useState } from 'react'
44
import dynamic from 'next/dynamic'
55
import Image from 'next/image'
66
import Link from 'next/link'
@@ -171,8 +171,8 @@ function YouCursor({ x, y, visible }: YouCursorProps) {
171171
* Collaboration section — team workflows and real-time collaboration.
172172
*
173173
* SEO:
174-
* - `<section id="collaboration" aria-labelledby="collaboration-heading">`.
175-
* - `<h2 id="collaboration-heading">` for the section title.
174+
* - `<section id="collaboration">` is the stable anchor for in-page navigation.
175+
* - The section title `<h2>` is linked via `aria-labelledby` using a `useId()`-generated id.
176176
* - Product visuals use `<figure>` with `<figcaption>` and descriptive `alt` text.
177177
*
178178
* GEO:
@@ -181,41 +181,17 @@ function YouCursor({ x, y, visible }: YouCursorProps) {
181181
* - Reference "Sim" by name per capability ("Sim's real-time collaboration").
182182
*/
183183

184-
const CURSOR_LERP_FACTOR = 0.3
185-
186184
export default function Collaboration() {
185+
const headingId = useId()
187186
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 })
188187
const [isHovering, setIsHovering] = useState(false)
189188
const sectionRef = useRef<HTMLElement>(null)
190-
const targetPos = useRef({ x: 0, y: 0 })
191-
const animationRef = useRef<number>(0)
192-
193-
useEffect(() => {
194-
const animate = () => {
195-
setCursorPos((prev) => ({
196-
x: prev.x + (targetPos.current.x - prev.x) * CURSOR_LERP_FACTOR,
197-
y: prev.y + (targetPos.current.y - prev.y) * CURSOR_LERP_FACTOR,
198-
}))
199-
animationRef.current = requestAnimationFrame(animate)
200-
}
201-
202-
if (isHovering) {
203-
animationRef.current = requestAnimationFrame(animate)
204-
}
205-
206-
return () => {
207-
if (animationRef.current) {
208-
cancelAnimationFrame(animationRef.current)
209-
}
210-
}
211-
}, [isHovering])
212189

213190
const handleMouseMove = useCallback((e: React.MouseEvent) => {
214-
targetPos.current = { x: e.clientX, y: e.clientY }
191+
setCursorPos({ x: e.clientX, y: e.clientY })
215192
}, [])
216193

217194
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
218-
targetPos.current = { x: e.clientX, y: e.clientY }
219195
setCursorPos({ x: e.clientX, y: e.clientY })
220196
setIsHovering(true)
221197
}, [])
@@ -228,7 +204,7 @@ export default function Collaboration() {
228204
<section
229205
ref={sectionRef}
230206
id='collaboration'
231-
aria-labelledby='collaboration-heading'
207+
aria-labelledby={headingId}
232208
className='bg-[var(--landing-bg)]'
233209
style={{ cursor: isHovering ? 'none' : 'auto' }}
234210
onMouseMove={handleMouseMove}
@@ -258,7 +234,7 @@ export default function Collaboration() {
258234
</Badge>
259235

260236
<h2
261-
id='collaboration-heading'
237+
id={headingId}
262238
className='text-balance font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
263239
>
264240
Realtime

apps/sim/app/(landing)/components/footer/footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface FooterItem {
1414
}
1515

1616
const PRODUCT_LINKS: FooterItem[] = [
17-
{ label: 'Mothership', href: 'https://docs.sim.ai', external: true },
17+
{ label: 'Mothership', href: 'https://docs.sim.ai/mothership', external: true },
1818
{ label: 'Workflows', href: 'https://docs.sim.ai', external: true },
1919
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
2020
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },

apps/sim/app/api/mcp/serve/[serverId]/route.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,51 @@ describe('MCP Serve Route', () => {
197197
expect(headers['X-API-Key']).toBeUndefined()
198198
expect(mockGenerateInternalToken).toHaveBeenCalledWith('user-1')
199199
})
200+
201+
describe('initialize protocol version negotiation', () => {
202+
async function callInitialize(protocolVersion?: string) {
203+
dbChainMockFns.limit.mockResolvedValueOnce([
204+
{
205+
id: 'server-1',
206+
name: 'Public Server',
207+
workspaceId: 'ws-1',
208+
isPublic: true,
209+
createdBy: 'owner-1',
210+
},
211+
])
212+
const params: Record<string, unknown> = {
213+
capabilities: {},
214+
clientInfo: { name: 'test', version: '1.0.0' },
215+
}
216+
if (protocolVersion !== undefined) params.protocolVersion = protocolVersion
217+
const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
218+
method: 'POST',
219+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params }),
220+
})
221+
const res = await POST(req, { params: Promise.resolve({ serverId: 'server-1' }) })
222+
return res.json() as Promise<{ result: { protocolVersion: string } }>
223+
}
224+
225+
it('echoes a supported client protocolVersion (2025-06-18)', async () => {
226+
const body = await callInitialize('2025-06-18')
227+
expect(body.result.protocolVersion).toBe('2025-06-18')
228+
})
229+
230+
it('echoes a supported client protocolVersion (2024-11-05)', async () => {
231+
const body = await callInitialize('2024-11-05')
232+
expect(body.result.protocolVersion).toBe('2024-11-05')
233+
})
234+
235+
it('falls back to SDK latest when client requests unknown version', async () => {
236+
const { LATEST_PROTOCOL_VERSION } = await import('@modelcontextprotocol/sdk/types.js')
237+
const body = await callInitialize('2099-01-01')
238+
expect(body.result.protocolVersion).toBe(LATEST_PROTOCOL_VERSION)
239+
})
240+
241+
it('falls back to SDK latest when client omits protocolVersion', async () => {
242+
const { LATEST_PROTOCOL_VERSION } = await import('@modelcontextprotocol/sdk/types.js')
243+
const body = await callInitialize(undefined)
244+
expect(body.result.protocolVersion).toBe(LATEST_PROTOCOL_VERSION)
245+
})
246+
})
200247
})

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
type JSONRPCError,
1212
type JSONRPCMessage,
1313
type JSONRPCResultResponse,
14+
LATEST_PROTOCOL_VERSION,
1415
type ListToolsResult,
1516
type RequestId,
17+
SUPPORTED_PROTOCOL_VERSIONS,
1618
type Tool,
1719
} from '@modelcontextprotocol/sdk/types.js'
1820
import { db } from '@sim/db'
@@ -36,6 +38,17 @@ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
3638

3739
const logger = createLogger('WorkflowMcpServeAPI')
3840

41+
function negotiateProtocolVersion(rpcParams: unknown): string {
42+
const requested =
43+
rpcParams && typeof rpcParams === 'object' && 'protocolVersion' in rpcParams
44+
? (rpcParams as { protocolVersion?: unknown }).protocolVersion
45+
: undefined
46+
if (typeof requested === 'string' && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
47+
return requested
48+
}
49+
return LATEST_PROTOCOL_VERSION
50+
}
51+
3952
export const dynamic = 'force-dynamic'
4053

4154
interface RouteParams {
@@ -214,7 +227,7 @@ export const POST = withRouteHandler(
214227
switch (method) {
215228
case 'initialize': {
216229
const result: InitializeResult = {
217-
protocolVersion: '2024-11-05',
230+
protocolVersion: negotiateProtocolVersion(rpcParams),
218231
capabilities: { tools: {} },
219232
serverInfo: { name: server.name, version: '1.0.0' },
220233
}

apps/sim/app/api/tools/hubspot/lists/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotListsSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2728
if (!parsed.success) return parsed.response
2829
const { credentialId, objectTypeId, query } = parsed.data.query
2930

31+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
32+
if (!credentialIdValidation.isValid) {
33+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
34+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
35+
}
36+
3037
const authz = await authorizeCredentialUse(request, {
3138
credentialId,
3239
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/owners/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotOwnersSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2728
if (!parsed.success) return parsed.response
2829
const { credentialId, query } = parsed.data.query
2930

31+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
32+
if (!credentialIdValidation.isValid) {
33+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
34+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
35+
}
36+
3037
const authz = await authorizeCredentialUse(request, {
3138
credentialId,
3239
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/pipelines/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotPipelinesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -33,6 +34,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3334
if (!parsed.success) return parsed.response
3435
const { credentialId, objectType } = parsed.data.query
3536

37+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
38+
if (!credentialIdValidation.isValid) {
39+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
40+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
41+
}
42+
3643
const authz = await authorizeCredentialUse(request, {
3744
credentialId,
3845
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/properties/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotPropertiesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -36,6 +37,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3637
if (!parsed.success) return parsed.response
3738
const { credentialId, objectType, query } = parsed.data.query
3839

40+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
41+
if (!credentialIdValidation.isValid) {
42+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
43+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
44+
}
45+
3946
const authz = await authorizeCredentialUse(request, {
4047
credentialId,
4148
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/mongodb/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { MongoClient } from 'mongodb'
2-
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
2+
import {
3+
createPinnedLookup,
4+
validateDatabaseHost,
5+
} from '@/lib/core/security/input-validation.server'
36
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
47

58
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
@@ -30,6 +33,7 @@ export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
3033
connectTimeoutMS: 10000,
3134
socketTimeoutMS: 10000,
3235
maxPoolSize: 1,
36+
lookup: createPinnedLookup(hostValidation.resolvedIP ?? config.host),
3337
})
3438

3539
await client.connect()

0 commit comments

Comments
 (0)