diff --git a/src/app/integrations/mcp/settings/use.ts b/src/app/integrations/mcp/settings/use.ts index 7244dc870..1edc47def 100644 --- a/src/app/integrations/mcp/settings/use.ts +++ b/src/app/integrations/mcp/settings/use.ts @@ -101,6 +101,10 @@ export function useMCPConnectionSettings( const connection = services.save({ ...target, enabled: false }) if (target.authenticationType === 'none') await services.setCredential(connection.id, '') else if (token.trim()) await services.setCredential(connection.id, token) + else if (target.enabled && (await services.status(connection.id)) !== 'configured') { + throw new Error(automation.value.bearerTokenRequired) + } + services.save({ ...target, id: connection.id }) }) if (!current(request)) return false diff --git a/src/app/integrations/storage/settings/use.ts b/src/app/integrations/storage/settings/use.ts index b360ed96b..787b26019 100644 --- a/src/app/integrations/storage/settings/use.ts +++ b/src/app/integrations/storage/settings/use.ts @@ -42,10 +42,18 @@ export function useStorageSettings(credentialDrafts: Ref> async function refreshStatuses(): Promise { const request = ++statusRequest - const id = provider.value.id - const statuses = await storageCredentialStatuses(id) - if (!disposed && statusRequest === request && provider.value.id === id) - credentialStatuses.value = statuses + const target = provider.value + try { + const statuses = await storageCredentialStatuses(target.id) + if (!disposed && statusRequest === request && provider.value.id === target.id) + credentialStatuses.value = statuses + } catch { + if (!disposed && statusRequest === request && provider.value.id === target.id) { + credentialStatuses.value = Object.fromEntries( + target.credentialFields.map((field) => [field.id, 'unavailable' as const]) + ) + } + } } function savePreferences(): void { diff --git a/tests/engine/app/settings/mcp-workflow.test.ts b/tests/engine/app/settings/mcp-workflow.test.ts index 108880cd9..612d164be 100644 --- a/tests/engine/app/settings/mcp-workflow.test.ts +++ b/tests/engine/app/settings/mcp-workflow.test.ts @@ -280,3 +280,41 @@ test('reopened editors serialize writes to the same connection while other conne secondScope.stop() } }) + +test('blank-token save rechecks status after a different editor clears the credential', async () => { + const scope = effectScope() + let configured = true + const enabled: boolean[] = [] + const services = { + status: async () => (configured ? ('configured' as const) : ('missing' as const)), + setCredential: async (_id: string, value: string) => { + configured = Boolean(value) + }, + save: (draft: { enabled: boolean }) => { + enabled.push(draft.enabled) + return connection + }, + remove: async () => undefined + } + try { + const first = scope.run(() => + useMCPConnectionSettings(ref(''), ref({ bearerTokenRequired: 'Required' }), services) + ) + const second = scope.run(() => + useMCPConnectionSettings(ref(''), ref({ bearerTokenRequired: 'Required' }), services) + ) + if (!first || !second) throw new Error('Missing scope') + await first.startEdit(connection.id) + await second.startEdit(connection.id) + second.draft.value.authenticationType = 'bearer' + second.draft.value.enabled = true + const clearing = first.clearCredential() + const saving = second.save() + await clearing + expect(await saving).toBe(false) + expect(second.error.value).toBe('Required') + expect(enabled).toEqual([false, false]) + } finally { + scope.stop() + } +})