Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bd636d4
feat(mcp): reuse warm connections for tool execution and discovery (#…
waleedlatif1 Jul 19, 2026
e337308
fix(mcp): keep a legacy server row from blanking the whole server lis…
waleedlatif1 Jul 19, 2026
fb439c9
fix(mcp): deliver OAuth callback result over BroadcastChannel so COOP…
waleedlatif1 Jul 19, 2026
de4acb3
chore(tiktok): simplify to draft-only Content Posting (#5703)
BillLeoutsakosvl346 Jul 20, 2026
58c87b2
feat(copilot): add share_file tool handler and surface file share sta…
TheodoreSpeaks Jul 20, 2026
4d76385
improvement(mcp): trace OAuth state writes to diagnose invalid_state …
waleedlatif1 Jul 20, 2026
031a295
fix(gmail-poller): resolve OAuth credential provider aliases (#5770)
j15z Jul 20, 2026
c6e8aac
fix(sidebar): reset drag state on dragend so edge drop zones can't st…
waleedlatif1 Jul 20, 2026
d9e793c
improvement(mcp): bound pool create-invalidation to in-flight creates…
waleedlatif1 Jul 20, 2026
6e5356b
improvement(blocks): name the integration in every suggested-action t…
waleedlatif1 Jul 20, 2026
922515f
fix(mcp): bound OAuth discovery/DCR/token fetches with a timeout (#5776)
waleedlatif1 Jul 20, 2026
d71e348
fix(ui): tuck notification stack below modal and dropdown menus (#5779)
waleedlatif1 Jul 20, 2026
2c4d209
feat(pi): add code review mode (#5577)
BillLeoutsakosvl346 Jul 20, 2026
a1ea3bd
fix(library): show full unambiguous dates on featured and related car…
waleedlatif1 Jul 20, 2026
ae6a363
improvement(ux): submit on Enter in chip modals and advance table row…
waleedlatif1 Jul 20, 2026
70a7bf0
diag(mcp): comprehensive HTTP logging for OAuth + streamable-HTTP tra…
waleedlatif1 Jul 20, 2026
53c2716
feat(blocks): surface deprecated block and model warnings on canvas (…
TheodoreSpeaks Jul 21, 2026
000c8f6
zip checkpoint
Sg312 Jul 1, 2026
d0eb9e5
fix(zip-uploads): harden extract path from review findings
Sg312 Jul 10, 2026
422d4ac
fix(zip-uploads): address review follow-ups on the extract path
Sg312 Jul 10, 2026
06d7aaa
fix(zip-uploads): detect prior nested-only extractions in the re-extr…
Sg312 Jul 10, 2026
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
110 changes: 70 additions & 40 deletions apps/docs/content/docs/en/workflows/blocks/pi.mdx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function ContentAuthorPage({
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>

Expand All @@ -82,6 +83,7 @@ export function ContentAuthorPage({
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>
<h3 className='text-[var(--text-primary)] text-base leading-tight tracking-[-0.01em] lg:text-lg'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ export function ContentIndexPage({
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
Expand Down Expand Up @@ -113,6 +115,7 @@ export function ContentIndexPage({
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>

Expand All @@ -122,6 +125,7 @@ export function ContentIndexPage({
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>
<h3 className='text-[var(--text-primary)] text-base leading-tight tracking-[-0.01em] lg:text-lg'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function ContentPostPage({
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
Expand Down Expand Up @@ -152,7 +153,9 @@ export function ContentPostPage({
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</span>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
Expand Down
9 changes: 6 additions & 3 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@
--auth-primary-btn-hover-border: #e0e0e0;
--auth-primary-btn-hover-text: #000000;

/* z-index scale. Transient poppers (menus, selects, popovers, tooltips, toasts)
sit above --z-modal so they stay clickable over the semi-transparent overlay. */
/* z-index scale. The toast/notification stack is ambient: it sits above page
content but BELOW the modal and the transient poppers (menus, selects,
popovers, tooltips), so an open modal or dropdown menu is never occluded by
a background notification. Poppers sit above --z-modal so they stay
clickable over the modal's semi-transparent overlay. */
--z-dropdown: 100;
--z-toast: 150;
--z-modal: 200;
--z-popover: 300;
--z-tooltip: 400;
--z-toast: 500;

/* Shadow scale */
--shadow-subtle: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/app/api/files/presigned/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ describe('/api/files/presigned', () => {

const response = await POST(request)
expect(response.status).toBe(200)
expect(mockValidateAttachmentFileType).toHaveBeenCalledWith('screenshot.png')
expect(mockValidateAttachmentFileType).toHaveBeenCalledWith('screenshot.png', {
allowArchives: true,
})
expect(mockValidateFileType).not.toHaveBeenCalled()
})

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/files/presigned/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

const fileValidationError = validateAttachmentFileType(fileName)
const fileValidationError = validateAttachmentFileType(fileName, { allowArchives: true })
if (fileValidationError) {
throw new ValidationError(fileValidationError.message)
}
Expand Down
9 changes: 6 additions & 3 deletions apps/sim/app/api/files/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { StorageContext } from '@/lib/uploads/config'
import { generateKnowledgeBaseFileKey } from '@/lib/uploads/contexts/knowledge-base/knowledge-base-file-manager'
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { MAX_WORKSPACE_FORMDATA_FILE_SIZE } from '@/lib/uploads/shared/types'
import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
import { isArchiveFileName, isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
import {
SUPPORTED_ATTACHMENT_EXTENSIONS,
SUPPORTED_IMAGE_EXTENSIONS,
Expand All @@ -34,9 +34,12 @@ import { createErrorResponse, InvalidRequestError } from '@/app/api/files/utils'

const ALLOWED_EXTENSIONS = new Set<string>(SUPPORTED_ATTACHMENT_EXTENSIONS)

function validateFileExtension(filename: string): boolean {
function validateFileExtension(filename: string, context: StorageContext): boolean {
const extension = filename.split('.').pop()?.toLowerCase()
if (!extension) return false
// Archives are only extractable in the mothership copilot flow; every other
// context keeps rejecting them up front instead of failing downstream.
if (context === 'mothership' && isArchiveFileName(filename)) return true
return ALLOWED_EXTENSIONS.has(extension)
}

Expand Down Expand Up @@ -150,7 +153,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
for (const file of files) {
const originalName = file.name || 'untitled.md'

if (!validateFileExtension(originalName)) {
if (!validateFileExtension(originalName, context)) {
const extension = originalName.split('.').pop()?.toLowerCase() || 'unknown'
throw new InvalidRequestError(
`File type '${extension}' is not allowed. Allowed types: ${Array.from(ALLOWED_EXTENSIONS).join(', ')}`
Expand Down
41 changes: 41 additions & 0 deletions apps/sim/app/api/mcp/oauth/callback/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,45 @@ describe('MCP OAuth callback route', () => {
})
)
})

it('signals success over a same-origin BroadcastChannel carrying the state nonce', async () => {
const request = new NextRequest(
'http://localhost:3000/api/mcp/oauth/callback?state=state-1&code=auth-code-1'
)

const body = await (await GET(request)).text()

// The completion is delivered over a BroadcastChannel (not window.opener.postMessage)
// so a COOP `same-origin` provider that severs the opener can't strand the parent. The
// `state` nonce lets the hook react only in the tab that started this exact flow.
expect(body).toContain("new BroadcastChannel('mcp-oauth')")
expect(body).toContain('ok: true')
expect(body).toContain('"server-1"')
expect(body).toContain('"state-1"')
})

it('reports an early failure over the channel without attempting token exchange', async () => {
// Missing `code` fails at the param gate, before any network work.
const request = new NextRequest('http://localhost:3000/api/mcp/oauth/callback?state=state-1')

const body = await (await GET(request)).text()

expect(body).toContain('ok: false')
expect(mcpOauthMockFns.mockMcpAuthGuarded).not.toHaveBeenCalled()
})

it('echoes the state on a serverless invalid_state failure so the initiating tab can react', async () => {
// No row loads for the state -> failure with no serverId. The state must still be echoed,
// or the initiating tab would sit on "Connecting…" until its safety timeout.
mcpOauthMockFns.mockLoadOauthRowByState.mockResolvedValueOnce(null)
const request = new NextRequest(
'http://localhost:3000/api/mcp/oauth/callback?state=state-1&code=auth-code-1'
)

const body = await (await GET(request)).text()

expect(body).toContain('ok: false')
expect(body).toContain('"state-1"')
expect(body).toContain('serverId: undefined')
})
})
59 changes: 38 additions & 21 deletions apps/sim/app/api/mcp/oauth/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,24 @@ function htmlClose(
message: string,
ok: boolean,
reason: McpOauthCallbackReason,
serverId?: string
serverId?: string,
state?: string
): NextResponse {
if (!ok) {
logger.warn(
`MCP OAuth callback did not complete: ${reason}${serverId ? ` (server ${serverId})` : ''}`
)
}
const safeMessage = escapeHtml(message)
const title = ok ? 'Connected' : 'Connection failed'
// Signal the opener over a same-origin BroadcastChannel rather than
// `window.opener.postMessage`: a provider whose authorize page sets COOP
// `same-origin` severs `window.opener`, which would silently drop the result and
// leave the parent stuck on "Connecting…". A BroadcastChannel is origin-scoped and
// unaffected by opener severance; the hook correlates on `state` and ignores flows it
// did not start.
const body = `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family: system-ui; padding: 24px"><p>${safeMessage}</p><script>
try { window.opener && window.opener.postMessage({ type: 'mcp-oauth', ok: ${ok ? 'true' : 'false'}, serverId: ${jsonLiteral(serverId)}, reason: ${jsonLiteral(reason)} }, window.location.origin) } catch (e) {}
try { var ch = new BroadcastChannel('mcp-oauth'); ch.postMessage({ type: 'mcp-oauth', ok: ${ok ? 'true' : 'false'}, serverId: ${jsonLiteral(serverId)}, state: ${jsonLiteral(state)}, reason: ${jsonLiteral(reason)} }); ch.close() } catch (e) {}
setTimeout(function () { window.close() }, 800)
</script></body></html>`
return new NextResponse(body, {
Expand All @@ -63,21 +75,26 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}
const { state, code, error: errorParam } = parsed.data.query

// Echo the flow's `state` on every result so the opener can correlate a broadcast back to
// the exact flow it started — including failures (e.g. `invalid_state`) that never resolve
// a serverId. Without it those results would strand the initiating tab on "Connecting…".
const respond = (
message: string,
ok: boolean,
reason: McpOauthCallbackReason,
serverId?: string
) => htmlClose(message, ok, reason, serverId, state)

const initialRow = state ? await loadOauthRowByState(state).catch(() => null) : null
const stateRowServerId = initialRow?.mcpServerId

if (errorParam) {
logger.warn(`MCP OAuth callback received error: ${errorParam}`)
if (initialRow) await clearState(initialRow.id).catch(() => {})
return htmlClose(
`Authorization failed: ${errorParam}`,
false,
'provider_error',
stateRowServerId
)
if (initialRow) await clearState(initialRow.id, 'callback:provider_error').catch(() => {})
return respond(`Authorization failed: ${errorParam}`, false, 'provider_error', stateRowServerId)
}
if (!state || !code) {
return htmlClose(
return respond(
'Missing state or code in callback URL.',
false,
'missing_params',
Expand All @@ -89,7 +106,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
if (!session?.user?.id) {
return htmlClose(
return respond(
'You must be signed in to complete authorization.',
false,
'unauthenticated',
Expand All @@ -99,12 +116,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {

const row = initialRow
if (!row) {
return htmlClose('Invalid or expired authorization state.', false, 'invalid_state')
return respond('Invalid or expired authorization state.', false, 'invalid_state')
}
serverId = row.mcpServerId

if (session.user.id !== row.userId) {
return htmlClose(
return respond(
'You must be signed in as the same user that initiated the flow.',
false,
'user_mismatch',
Expand All @@ -118,10 +135,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
.where(and(eq(mcpServers.id, row.mcpServerId), isNull(mcpServers.deletedAt)))
.limit(1)
if (!server || !server.url) {
return htmlClose('Server no longer exists.', false, 'server_gone', serverId)
return respond('Server no longer exists.', false, 'server_gone', serverId)
}
if (server.workspaceId !== row.workspaceId) {
return htmlClose(
return respond(
'Workspace mismatch on authorization callback.',
false,
'invalid_state',
Expand All @@ -131,7 +148,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
try {
assertSafeOauthServerUrl(server.url)
} catch {
return htmlClose(
return respond(
'MCP OAuth requires https (or http://localhost for development).',
false,
'insecure_url',
Expand All @@ -140,7 +157,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}

// Burn state before token exchange so a replayed callback cannot reuse it.
await clearState(row.id)
await clearState(row.id, 'callback:burn-before-exchange')

const preregistered = await loadPreregisteredClient(server.id)
const provider = new SimMcpOauthProvider({ row, preregistered })
Expand All @@ -152,7 +169,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
})
} catch (e) {
logger.error('Token exchange failed during MCP OAuth callback', e)
return htmlClose(
return respond(
'Token exchange failed. Please try again.',
false,
'token_exchange_failed',
Expand All @@ -163,7 +180,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}

if (result !== 'AUTHORIZED') {
return htmlClose('Authorization did not complete.', false, 'token_exchange_failed', server.id)
return respond('Authorization did not complete.', false, 'token_exchange_failed', server.id)
}

try {
Expand All @@ -173,9 +190,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
logger.warn('Post-auth tools refresh failed', toError(e).message)
}

return htmlClose('Connected. You can close this window.', true, 'authorized', server.id)
return respond('Connected. You can close this window.', true, 'authorized', server.id)
} catch (error) {
logger.error('MCP OAuth callback failed', error)
return htmlClose('Authorization failed. Please try again.', false, 'unknown', serverId)
return respond('Authorization failed. Please try again.', false, 'unknown', serverId)
}
})
7 changes: 7 additions & 0 deletions apps/sim/app/api/mcp/oauth/start/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const MAX_SURFACED_ERROR_LENGTH = 250
const DCR_UNSUPPORTED_MESSAGE =
"This server doesn't support automatic OAuth client registration. Add a pre-registered OAuth client ID and secret, or configure a token instead."

/**
* The MCP SDK throws a plain `Error` (no typed class or code) when an auth server lacks a
* `registration_endpoint`, so this string-match is the only available signal. Pinned to
* `@modelcontextprotocol/sdk` v1.29.0 `registerClient` (client/auth.js): "Incompatible auth
* server: does not support dynamic client registration". If a version bump rephrases this, the
* 422 branch silently stops firing (users get a generic 500) — update the substring here.
*/
function isDynamicClientRegistrationUnsupported(error: unknown): boolean {
return getErrorMessage(error, '')
.toLowerCase()
Expand Down
Loading
Loading