${safeMessage}
` return new NextResponse(body, { @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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 }) @@ -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', @@ -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 { @@ -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) } }) diff --git a/apps/sim/app/api/mcp/oauth/start/route.ts b/apps/sim/app/api/mcp/oauth/start/route.ts index 882befca8d7..5bc08af493f 100644 --- a/apps/sim/app/api/mcp/oauth/start/route.ts +++ b/apps/sim/app/api/mcp/oauth/start/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts index 4a2b9e260b1..6b3632c19f7 100644 --- a/apps/sim/app/api/tools/file/manage/route.ts +++ b/apps/sim/app/api/tools/file/manage/route.ts @@ -1,5 +1,4 @@ import { Buffer, isUtf8 } from 'buffer' -import type { Readable } from 'stream' import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -21,6 +20,12 @@ import { ShareValidationError, upsertFileShare, } from '@/lib/public-shares/share-manager' +import { + ArchiveError, + type DecompressResult, + decompressArchiveBufferToWorkspaceFiles, + MAX_ARCHIVE_BYTES, +} from '@/lib/uploads/archive' import { ensureWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { fetchWorkspaceFileBuffer, @@ -203,102 +208,6 @@ const uniqueZipEntryName = (name: string, usedNames: Set
The "{props.toolName}" tool requires access to your account.
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
index 188ad323b3c..55d73124605 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
@@ -15,7 +15,7 @@ import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
-import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
+import { MOTHERSHIP_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import {
AnimatedPlaceholderEffect,
@@ -597,7 +597,7 @@ const UserInputImpl = forwardRef {copy.description} ,
params: P,
- memoryConfig: PiMemoryConfig
+ memoryConfig?: PiMemoryConfig
): Promise