feat(ai): implement MCP service integration for enhanced AI capabilities (#4359)

* feat(ai): implement MCP service integration for enhanced AI capabilities

- Added functionality to manage MCP services, including adding, updating, removing, and connecting to services.
- Introduced UI components for MCP service management, allowing users to enable MCP services and discover service endpoints.
- Updated AI settings to include MCP service configurations and integrated translations for new UI elements.
- Enhanced error handling and user feedback for service connection and discovery processes.

These changes collectively extend the AI capabilities by integrating external services through secure OAuth connections, improving the overall user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* feat(mcp): update MCP service integration and enhance settings management

- Upgraded '@follow-app/client-sdk' to version 0.3.38, reflecting the latest changes in MCP service management.
- Removed deprecated MCP service functions and streamlined the integration process for better performance.
- Introduced new UI components for managing MCP connections, including transport type selection and header configuration.
- Enhanced error handling and user feedback for service connection and discovery processes.
- Updated translations for new UI elements related to MCP services.

These changes collectively improve the user experience by providing a more robust and intuitive interface for managing external services through secure OAuth connections.

Signed-off-by: Innei <tukon479@gmail.com>

* feat(ai-chat): enhance ToolInvocationComponent with error handling and UI updates

- Added error handling to ToolInvocationComponent, displaying error messages and visual indicators when tool execution fails.
- Updated the UI to reflect tool status, changing icons and text based on success or failure.
- Improved styling for the back button in ChatInterfaceContent for a more consistent appearance.

These changes improve user feedback and interaction within the AI chat interface, enhancing overall usability.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(ai): streamline MCPServiceItem layout for improved readability

- Removed redundant resource and prompt counts from the MCPServiceItem component to simplify the display.
- Adjusted the layout to enhance the presentation of service details, including tools, creation date, and last used date.

These changes improve the clarity and usability of the MCP service information within the AI settings interface.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update deps

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-08-19 23:34:19 +08:00 committed by GitHub
parent 9872cff3f5
commit 0c9977d5d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 948 additions and 119 deletions

View File

@ -1,6 +1,6 @@
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
import { defaultAISettings } from "@follow/shared/settings/defaults"
import type { AISettings } from "@follow/shared/settings/interface"
import type { AISettings, MCPService } from "@follow/shared/settings/interface"
import { jotaiStore } from "@follow/utils"
import { atom, useAtomValue } from "jotai"
@ -70,6 +70,37 @@ export const setAIPanelVisibility = (visibility: boolean) => {
}
export const getAIPanelVisibility = () => jotaiStore.get(aiPanelVisibilityAtom)
////////// MCP Services
export const useMCPEnabled = () => useAISettingKey("mcpEnabled")
export const setMCPEnabled = (enabled: boolean) => {
setAISetting("mcpEnabled", enabled)
}
export const useMCPServices = () => useAISettingKey("mcpServices")
export const addMCPService = (service: Omit<MCPService, "id">) => {
const services = getAISettings().mcpServices
const newService = {
...service,
id: Date.now().toString(),
}
setAISetting("mcpServices", [...services, newService])
return newService.id
}
export const updateMCPService = (id: string, updates: Partial<MCPService>) => {
const services = getAISettings().mcpServices
const updatedServices = services.map((service) =>
service.id === id ? { ...service, ...updates } : service,
)
setAISetting("mcpServices", updatedServices)
}
export const removeMCPService = (id: string) => {
const services = getAISettings().mcpServices
const filteredServices = services.filter((service) => service.id !== id)
setAISetting("mcpServices", filteredServices)
}
//// Enhance Init Ai Settings
export const initializeDefaultAISettings = () => {
initializeDefaultSettings()

View File

@ -215,8 +215,8 @@ const ChatInterfaceContent = () => {
onClick={() => resetScrollState()}
className={cn(
"backdrop-blur-background group flex items-center gap-2 rounded-full border px-3.5 py-2 transition-all",
"border-border/40 bg-material-ultra-thin/70 shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"hover:bg-material-thin/70 hover:border-border/60 active:scale-[0.98]",
"border-border/40 bg-material-ultra-thin shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"hover:bg-material-medium hover:border-border/60 active:scale-[0.98]",
)}
>
<i className="i-mingcute-arrow-down-line text-text/90 size-3" />

View File

@ -16,9 +16,14 @@ interface ToolInvocationComponentProps {
export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = React.memo(
({ part }) => {
const toolName = getToolName(part)
const hasError = "errorText" in part && part.errorText
return (
<div className="bg-material-medium border-border size-full min-w-0 max-w-prose rounded-lg border text-left">
<div
className={`bg-material-medium size-full min-w-0 max-w-prose rounded-lg border text-left ${
hasError ? "border-red/30" : "border-border"
}`}
>
<div className="w-[9999px] max-w-[calc(var(--ai-chat-layout-width,65ch)_-120px)]" />
<Accordion type="single" collapsible>
<AccordionItem value="tool-invocation">
@ -26,9 +31,15 @@ export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = R
{/* Tool Info */}
<div className="flex h-6 min-w-0 flex-1 items-center">
<div className="flex items-center gap-2 text-xs">
<i className="i-mingcute-tool-line" />
<span className="text-text-secondary">Tool Calling:</span>
<h4 className="text-text truncate font-medium">{toolName}</h4>
<i
className={hasError ? "i-mgc-close-cute-re text-red" : "i-mingcute-tool-line"}
/>
<span className="text-text-secondary">
{hasError ? "Tool Failed:" : "Tool Calling:"}
</span>
<h4 className={`truncate font-medium ${hasError ? "text-red" : "text-text"}`}>
{toolName}
</h4>
</div>
</div>
</AccordionTrigger>
@ -44,7 +55,7 @@ export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = R
</div>
)}
{"output" in part && (
{"output" in part && !!part.output && (
<div>
<div className="text-text-tertiary mb-2 text-xs font-semibold uppercase tracking-wide">
Result
@ -52,6 +63,25 @@ export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = R
<JsonHighlighter json={JSON.stringify(part.output, null, 2)} />
</div>
)}
{hasError && (
<div>
<div className="text-red mb-2 text-xs font-semibold uppercase tracking-wide">
Error
</div>
<div className="bg-red/5 border-red/20 text-red rounded-lg border p-3 text-sm">
<div className="flex items-start gap-2">
<i className="i-mgc-warning-cute-re mt-0.5 flex-shrink-0 text-base" />
<div className="min-w-0">
<div className="font-medium">Tool Execution Failed</div>
<div className="text-red/80 mt-1 break-words font-mono text-xs">
{"errorText" in part ? part.errorText : ""}
</div>
</div>
</div>
</div>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>

View File

@ -1,7 +1,13 @@
import type { PrimitiveAtom } from "jotai"
import { atom } from "jotai"
import { createContext } from "react"
import { createAtomHooks } from "~/lib/jotai"
export const [, , useSettingTab, useSetSettingTab, getSettingTab, setSettingTab] = createAtomHooks(
atom(""),
)
export const SettingModalContentPortalableContext = createContext<PrimitiveAtom<HTMLElement>>(
atom(null as any),
)

View File

@ -5,10 +5,12 @@ import { LetsIconsResizeDownRightLight } from "@follow/components/icons/resize.j
import { IN_ELECTRON } from "@follow/shared/constants"
import { preventDefault } from "@follow/utils/dom"
import { cn, getOS } from "@follow/utils/utils"
import { atom, useAtomValue, useSetAtom } from "jotai"
import type { BoundingBox } from "motion/react"
import { Resizable } from "re-resizable"
import type { PropsWithChildren } from "react"
import { memo, Suspense, useCallback, useEffect, useRef } from "react"
import { memo, Suspense, use, useCallback, useEffect, useMemo, useRef } from "react"
import { createPortal } from "react-dom"
import { useUISettingSelector } from "~/atoms/settings/ui"
import { m } from "~/components/common/Motion"
@ -24,7 +26,7 @@ import { useAvailableSettings, useSettingPageContext } from "../hooks/use-settin
import { SettingsSidebarTitle } from "../title"
import type { SettingPageConfig } from "../utils"
import { DisableWhy } from "../utils"
import { useSetSettingTab, useSettingTab } from "./context"
import { SettingModalContentPortalableContext, useSetSettingTab, useSettingTab } from "./context"
import { defaultCtx, SettingContext } from "./hooks"
export function SettingModalLayout(
@ -77,6 +79,10 @@ export function SettingModalLayout(
return constraints
}).current
const portalableCtxValue = useMemo(() => {
return atom(null as any)
}, [])
return (
<div
id={SETTING_MODAL_ID}
@ -142,7 +148,10 @@ export function SettingModalLayout(
</div>
</div>
<div className="bg-background relative flex h-full min-w-0 flex-1 flex-col pt-1">
<Suspense>{children}</Suspense>
<SettingModalContentPortalableContext value={portalableCtxValue}>
<Suspense>{children}</Suspense>
<SettingModalContentPortalable />
</SettingModalContentPortalableContext>
</div>
</div>
@ -154,6 +163,11 @@ export function SettingModalLayout(
)
}
const SettingModalContentPortalable = () => {
const setElement = useSetAtom(use(SettingModalContentPortalableContext))
return <div ref={setElement as any} />
}
const SettingItemButtonImpl = (props: {
setTab: (tab: string) => void
item: SettingPageConfig
@ -220,3 +234,8 @@ export const SidebarItems = memo((props: { onChange?: (tab: string) => void }) =
)
})
})
export const SettingModalContentPortal: Component = ({ children }) => {
const element = useAtomValue(use(SettingModalContentPortalableContext))
return createPortal(children, element)
}

View File

@ -1,9 +1,21 @@
import { Spring } from "@follow/components/constants/spring.js"
import { Button } from "@follow/components/ui/button/index.js"
import { Input, TextArea } from "@follow/components/ui/input/index.js"
import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js"
import { KeyValueEditor } from "@follow/components/ui/key-value-editor/index.js"
import { Label } from "@follow/components/ui/label/index.jsx"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@follow/components/ui/select/index.js"
import { Switch } from "@follow/components/ui/switch/index.jsx"
import type { AIShortcut } from "@follow/shared/settings/interface"
import type { AIShortcut, MCPService } from "@follow/shared/settings/interface"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { AnimatePresence } from "motion/react"
import * as React from "react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@ -12,13 +24,26 @@ import {
AIChatPanelStyle,
setAIChatPanelStyle,
setAISetting,
setMCPEnabled,
useAIChatPanelStyle,
useAISettingValue,
useMCPEnabled,
} from "~/atoms/settings/ai"
import { m } from "~/components/common/Motion"
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
import {
createMCPConnection,
deleteMCPConnection,
fetchMCPConnections,
mcpQueryKeys,
refreshMCPTools,
updateMCPConnection,
} from "~/queries/mcp"
import { SettingActionItem, SettingDescription, SettingTabbedSegment } from "../control"
import { createDefineSettingItem } from "../helper/builder"
import { createSettingBuilder } from "../helper/setting-builder"
import { SettingModalContentPortal } from "../modal/layout"
const SettingBuilder = createSettingBuilder(useAISettingValue)
const defineSettingItem = createDefineSettingItem(useAISettingValue, setAISetting)
@ -53,6 +78,12 @@ export const SettingAI = () => {
value: t("shortcuts.title"),
},
AIShortcutsSection,
{
type: "title",
value: t("integration.title"),
},
MCPServicesSection,
]}
/>
</div>
@ -126,17 +157,31 @@ const PersonalizePromptSetting = () => {
</SettingDescription>
</div>
<div className="flex h-9 justify-end">
<Button
onClick={handleSave}
disabled={isSaving || !hasChanges || isOverLimit}
buttonClassName={`transition-opacity duration-200 ${
hasChanges && !isOverLimit ? "opacity-100" : "pointer-events-none opacity-0"
}`}
>
{isSaving ? "Saving..." : "Save"}
</Button>
</div>
<AnimatePresence>
{hasChanges && (
<SettingModalContentPortal>
<m.div
initial={{ y: 20, scale: 0.95 }}
animate={{ y: 0, scale: 1 }}
exit={{ y: 20, scale: 0.95 }}
transition={Spring.presets.snappy}
className="absolute inset-x-0 bottom-3 z-10 flex justify-center px-3"
>
<div className="backdrop-blur-background bg-material-medium border-border shadow-perfect flex w-fit max-w-[92%] items-center justify-between gap-3 rounded-full border py-2 pl-5 pr-2">
<span className="text-text-secondary text-xs sm:text-sm">Unsaved changes</span>
<Button
buttonClassName="bg-accent rounded-full"
size="sm"
onClick={handleSave}
disabled={isSaving || isOverLimit}
>
{isSaving ? "Saving..." : "Save"}
</Button>
</div>
</m.div>
</SettingModalContentPortal>
)}
</AnimatePresence>
</div>
)
}
@ -144,20 +189,49 @@ const PersonalizePromptSetting = () => {
const AIShortcutsSection = () => {
const { t } = useTranslation("ai")
const { shortcuts } = useAISettingValue()
const [isCreating, setIsCreating] = useState(false)
const { present } = useModalStack()
const handleAddShortcut = () => {
setIsCreating(true)
present({
title: "Add AI Shortcut",
content: ({ dismiss }: { dismiss: () => void }) => (
<ShortcutModalContent
shortcut={null}
onSave={(shortcut) => {
const newShortcut: AIShortcut = {
...shortcut,
id: Date.now().toString(),
}
setAISetting("shortcuts", [...shortcuts, newShortcut])
toast.success(t("shortcuts.added"))
dismiss()
}}
onCancel={dismiss}
/>
),
})
}
const handleSaveShortcut = (shortcut: Omit<AIShortcut, "id">) => {
const newShortcut: AIShortcut = {
...shortcut,
id: Date.now().toString(),
}
setAISetting("shortcuts", [...shortcuts, newShortcut])
setIsCreating(false)
toast.success(t("shortcuts.added"))
const handleEditShortcut = (shortcut: AIShortcut) => {
present({
title: "Edit AI Shortcut",
content: ({ dismiss }: { dismiss: () => void }) => (
<ShortcutModalContent
shortcut={shortcut}
onSave={(updatedShortcut) => {
setAISetting(
"shortcuts",
shortcuts.map((s) =>
s.id === shortcut.id ? { ...updatedShortcut, id: shortcut.id } : s,
),
)
toast.success(t("shortcuts.updated"))
dismiss()
}}
onCancel={dismiss}
/>
),
})
}
const handleDeleteShortcut = (id: string) => {
@ -175,29 +249,15 @@ const AIShortcutsSection = () => {
)
}
const handleUpdateShortcut = (id: string, updatedShortcut: Omit<AIShortcut, "id">) => {
setAISetting(
"shortcuts",
shortcuts.map((s) => (s.id === id ? { ...updatedShortcut, id } : s)),
)
toast.success(t("shortcuts.updated"))
}
return (
<div className="space-y-4">
<SettingDescription>{t("shortcuts.description")}</SettingDescription>
<SettingActionItem
label={t("shortcuts.add")}
action={handleAddShortcut}
buttonText={t("shortcuts.add")}
/>
{isCreating && (
<ShortcutEditor onSave={handleSaveShortcut} onCancel={() => setIsCreating(false)} />
)}
{shortcuts.length === 0 && !isCreating && (
{shortcuts.length === 0 && (
<div className="py-8 text-center">
<div className="bg-fill-secondary mx-auto mb-3 flex size-12 items-center justify-center rounded-full">
<i className="i-mgc-magic-2-cute-re text-text size-6" />
@ -213,7 +273,7 @@ const AIShortcutsSection = () => {
shortcut={shortcut}
onDelete={handleDeleteShortcut}
onToggle={handleToggleShortcut}
onUpdate={handleUpdateShortcut}
onEdit={handleEditShortcut}
/>
))}
</div>
@ -224,29 +284,10 @@ interface ShortcutItemProps {
shortcut: AIShortcut
onDelete: (id: string) => void
onToggle: (id: string, enabled: boolean) => void
onUpdate: (id: string, shortcut: Omit<AIShortcut, "id">) => void
onEdit: (shortcut: AIShortcut) => void
}
const ShortcutItem = ({ shortcut, onDelete, onToggle, onUpdate }: ShortcutItemProps) => {
const [isEditing, setIsEditing] = useState(false)
const handleSave = (updatedShortcut: Omit<AIShortcut, "id">) => {
onUpdate(shortcut.id, updatedShortcut)
setIsEditing(false)
}
if (isEditing) {
return (
<div className="before:bg-accent relative pl-4 before:absolute before:inset-y-0 before:left-0 before:w-1 before:rounded-full before:content-['']">
<ShortcutEditor
shortcut={shortcut}
onSave={handleSave}
onCancel={() => setIsEditing(false)}
/>
</div>
)
}
const ShortcutItem = ({ shortcut, onDelete, onToggle, onEdit }: ShortcutItemProps) => {
return (
<div className="hover:bg-material-medium border-border group rounded-lg border p-4 transition-colors">
<div className="flex items-start justify-between">
@ -266,7 +307,7 @@ const ShortcutItem = ({ shortcut, onDelete, onToggle, onUpdate }: ShortcutItemPr
<div className="ml-4 flex items-center gap-3">
<div className="flex items-center gap-1 opacity-60 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="sm" onClick={() => setIsEditing(true)}>
<Button variant="ghost" size="sm" onClick={() => onEdit(shortcut)}>
<i className="i-mgc-edit-cute-re size-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onDelete(shortcut.id)}>
@ -289,13 +330,13 @@ const ShortcutItem = ({ shortcut, onDelete, onToggle, onUpdate }: ShortcutItemPr
)
}
interface ShortcutEditorProps {
shortcut?: AIShortcut
interface ShortcutModalContentProps {
shortcut?: AIShortcut | null
onSave: (shortcut: Omit<AIShortcut, "id">) => void
onCancel: () => void
}
const ShortcutEditor = ({ shortcut, onSave, onCancel }: ShortcutEditorProps) => {
const ShortcutModalContent = ({ shortcut, onSave, onCancel }: ShortcutModalContentProps) => {
const { t } = useTranslation("ai")
const [name, setName] = useState(shortcut?.name || "")
const [prompt, setPrompt] = useState(shortcut?.prompt || "")
@ -316,9 +357,9 @@ const ShortcutEditor = ({ shortcut, onSave, onCancel }: ShortcutEditorProps) =>
}
return (
<div className="bg-material-medium space-y-4 rounded-lg p-4">
<div className="w-[400px] space-y-4">
<div className="grid grid-cols-6 gap-4">
<div className="col-span-4 space-y-2">
<div className="col-span-6 space-y-2">
<Label className="text-text text-xs">{t("shortcuts.name")}</Label>
<Input
value={name}
@ -387,6 +428,491 @@ const ShortcutEditor = ({ shortcut, onSave, onCancel }: ShortcutEditorProps) =>
)
}
const MCPServicesSection = () => {
const { t } = useTranslation("ai")
const mcpEnabled = useMCPEnabled()
const queryClient = useQueryClient()
const dialog = useDialog()
// Reusable OAuth authorization handler using dialog
const handleOAuthAuthorization = async (authorizationUrl: string) => {
const confirmed = await dialog.ask({
title: t("integration.mcp.service.auth_required"),
message: t("integration.mcp.service.auth_message"),
confirmText: t("integration.mcp.service.open_auth"),
cancelText: t("words.cancel", { ns: "common" }),
variant: "ask",
})
if (confirmed) {
const popup = window.open(
authorizationUrl,
"_blank",
"width=600,height=700,scrollbars=yes,resizable=yes",
)
if (!popup) {
toast.error(t("integration.mcp.service.popup_blocked"))
} else {
toast.success(t("integration.mcp.service.auth_window_opened"))
}
}
}
// Query for MCP connections
const {
data: mcpServices = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: mcpQueryKeys.connections(),
queryFn: fetchMCPConnections,
enabled: mcpEnabled,
refetchInterval: 30_000,
refetchOnWindowFocus: true,
retry: 2,
})
// Mutation for creating MCP connection
const createConnectionMutation = useMutation({
mutationFn: createMCPConnection,
onSuccess: async (result) => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
// Handle OAuth authorization if needed
if (result.authorizationUrl) {
await handleOAuthAuthorization(result.authorizationUrl)
} else {
toast.success(t("integration.mcp.service.added"))
}
},
onError: (error) => {
toast.error(t("integration.mcp.service.discovery_failed"))
console.error("Failed to create MCP connection:", error)
},
})
// Mutation for updating MCP connection
const updateConnectionMutation = useMutation({
mutationFn: ({
connectionId,
updateData,
}: {
connectionId: string
updateData: Parameters<typeof updateMCPConnection>[1]
}) => updateMCPConnection(connectionId, updateData),
onSuccess: async (result) => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
// Handle OAuth authorization if needed
if (result.authorizationUrl) {
await handleOAuthAuthorization(result.authorizationUrl)
} else {
toast.success(t("integration.mcp.service.updated"))
}
},
onError: (error) => {
toast.error("Failed to update MCP connection")
console.error("Failed to update MCP connection:", error)
},
})
// Mutation for deleting MCP connection
const deleteConnectionMutation = useMutation({
mutationFn: deleteMCPConnection,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
toast.success(t("integration.mcp.service.deleted"))
},
onError: (error) => {
toast.error("Failed to delete MCP connection")
console.error("Failed to delete MCP connection:", error)
},
})
// Mutation for refreshing MCP tools
const refreshToolsMutation = useMutation({
mutationFn: (connectionIds?: string[]) => refreshMCPTools(connectionIds),
onSuccess: () => {
// Invalidate both connections (for updated counts) and tools queries
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.connections() })
queryClient.invalidateQueries({ queryKey: mcpQueryKeys.all })
toast.success("MCP tools refreshed successfully")
},
onError: (error) => {
toast.error("Failed to refresh MCP tools")
console.error("Failed to refresh MCP tools:", error)
},
})
const { present } = useModalStack()
const handleAddService = () => {
present({
title: "Add MCP Service",
content: ({ dismiss }: { dismiss: () => void }) => (
<MCPServiceModalContent
service={null}
onSave={(service) => {
createConnectionMutation.mutate(service)
dismiss()
}}
onCancel={dismiss}
isLoading={createConnectionMutation.isPending}
/>
),
})
}
const handleEditService = (service: MCPService) => {
present({
title: "Edit MCP Service",
content: ({ dismiss }: { dismiss: () => void }) => (
<MCPServiceModalContent
service={service}
onSave={(updatedService) => {
updateConnectionMutation.mutate({
connectionId: service.id,
updateData: updatedService,
})
dismiss()
}}
onCancel={dismiss}
isLoading={updateConnectionMutation.isPending}
/>
),
})
}
const handleDeleteService = (id: string) => {
deleteConnectionMutation.mutate(id)
}
const handleRefreshTools = (connectionId?: string) => {
refreshToolsMutation.mutate(connectionId ? [connectionId] : undefined)
}
// Show error message if query failed
React.useEffect(() => {
if (error) {
toast.error("Failed to load MCP connections")
console.error("Failed to load MCP connections:", error)
}
}, [error])
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-text text-sm font-medium">{t("integration.mcp.enabled")}</Label>
<div className="text-text-secondary text-xs">{t("integration.mcp.description")}</div>
</div>
<Switch checked={mcpEnabled} onCheckedChange={setMCPEnabled} />
</div>
</div>
{mcpEnabled && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-text text-sm font-medium">
{t("integration.mcp.services.title")}
</Label>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => refetch()}
disabled={isLoading}
title="Refresh connections"
>
{isLoading ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-refresh-2-cute-re size-4" />
)}
</Button>
<Button variant="outline" size="sm" onClick={handleAddService}>
<i className="i-mgc-add-cute-re mr-2 size-4" />
{t("integration.mcp.services.add")}
</Button>
</div>
</div>
{mcpServices.length === 0 && (
<div className="py-8 text-center">
<div className="bg-fill-secondary mx-auto mb-3 flex size-12 items-center justify-center rounded-full">
<i className="i-mgc-plugin-2-cute-re text-text size-6" />
</div>
<h4 className="text-text mb-1 text-sm font-medium">
{t("integration.mcp.services.empty.title")}
</h4>
<p className="text-text-secondary text-xs">
{t("integration.mcp.services.empty.description")}
</p>
</div>
)}
{isLoading && (
<div className="flex items-center justify-center py-8">
<i className="i-mgc-loading-3-cute-re size-6 animate-spin" />
</div>
)}
{mcpServices.map((service) => (
<MCPServiceItem
key={service.id}
service={service}
onDelete={handleDeleteService}
onRefresh={handleRefreshTools}
onEdit={handleEditService}
isDeleting={
deleteConnectionMutation.isPending &&
deleteConnectionMutation.variables === service.id
}
isRefreshing={
refreshToolsMutation.isPending && refreshToolsMutation.variables?.[0] === service.id
}
/>
))}
</div>
)}
</div>
)
}
interface MCPServiceItemProps {
service: MCPService
onDelete: (id: string) => void
onRefresh: (connectionId: string) => void
onEdit: (service: MCPService) => void
isDeleting?: boolean
isRefreshing?: boolean
}
const MCPServiceItem = ({
service,
onDelete,
onRefresh,
onEdit,
isDeleting = false,
isRefreshing = false,
}: MCPServiceItemProps) => {
const { t } = useTranslation("ai")
const getConnectionStatusColor = (isConnected: boolean) => {
return isConnected ? "bg-green/10 text-green" : "bg-gray/10 text-text-tertiary"
}
const getConnectionStatusText = (isConnected: boolean) => {
return isConnected
? t("integration.mcp.service.connected")
: t("integration.mcp.service.disconnected")
}
const formatDate = (dateString: string | null) => {
if (!dateString) return "Never"
return new Date(dateString).toLocaleDateString()
}
return (
<div className="hover:bg-material-medium border-border group rounded-lg border p-4 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h4 className="text-text text-sm font-medium">{service.name}</h4>
<div
className={`rounded-full px-2 py-1 text-xs ${getConnectionStatusColor(service.isConnected)}`}
>
{getConnectionStatusText(service.isConnected)}
</div>
<div className="bg-blue/10 text-blue rounded-full px-2 py-1 text-xs">
{service.transportType}
</div>
</div>
<div className="space-y-1">
{service.url && (
<p className="text-text-secondary text-xs">
<span className="text-text-tertiary">URL:</span> {service.url}
</p>
)}
<p className="text-text-secondary text-xs">
<span className="text-text-tertiary">Tools:</span> {service.toolCount}
<span className="text-text-tertiary ml-4">Created:</span>{" "}
{formatDate(service.createdAt)}
<span className="text-text-tertiary ml-4">Last Used:</span>{" "}
{formatDate(service.lastUsed)}
</p>
{service.lastError && (
<p className="text-red text-xs">
<span className="text-text-tertiary">Error:</span> {service.lastError}
</p>
)}
</div>
</div>
<div className="ml-4 flex items-center gap-1 opacity-60 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="sm" onClick={() => onEdit(service)} title="Edit connection">
<i className="i-mgc-edit-cute-re size-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onRefresh(service.id)}
title="Refresh tools"
disabled={isRefreshing}
>
{isRefreshing ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-refresh-2-cute-re size-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(service.id)}
disabled={isDeleting}
>
{isDeleting ? (
<i className="i-mgc-loading-3-cute-re size-4 animate-spin" />
) : (
<i className="i-mgc-delete-2-cute-re size-4" />
)}
</Button>
</div>
</div>
</div>
)
}
interface MCPServiceModalContentProps {
service?: MCPService | null
onSave: (service: {
name: string
transportType: "streamable-http" | "sse"
url: string
headers?: Record<string, string>
}) => void
onCancel: () => void
isLoading?: boolean
}
const MCPServiceModalContent = ({
service,
onSave,
onCancel,
isLoading = false,
}: MCPServiceModalContentProps) => {
const { t } = useTranslation("ai")
const [name, setName] = useState(service?.name || "")
const [url, setUrl] = useState(service?.url || "")
const [transportType, setTransportType] = useState<"streamable-http" | "sse">(
service?.transportType || "streamable-http",
)
const [headers, setHeaders] = useState<Record<string, string>>(service?.headers || {})
const handleSave = () => {
if (!name.trim()) {
toast.error(t("integration.mcp.service.validation.name_required"))
return
}
if (!url.trim()) {
toast.error(t("integration.mcp.service.validation.baseUrl_required"))
return
}
// Basic URL validation
try {
new URL(url.trim())
} catch {
toast.error(t("integration.mcp.service.validation.invalid_url"))
return
}
onSave({
name: name.trim(),
transportType,
url: url.trim(),
headers: Object.keys(headers).length > 0 ? headers : undefined,
})
}
return (
<div className="space-y-4">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label className="text-text text-xs">{t("integration.mcp.service.name")}</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("integration.mcp.service.name_placeholder")}
/>
</div>
<div className="space-y-2">
<Label className="text-text text-xs">Transport Type</Label>
<Select
value={transportType}
onValueChange={(value) => setTransportType(value as "streamable-http" | "sse")}
>
<SelectTrigger>
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="streamable-http">Streamable HTTP</SelectItem>
<SelectItem value="sse">Server-Sent Events</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-text text-xs">URL</Label>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/mcp"
/>
</div>
<div className="min-w-[500px] space-y-2">
<Label className="text-text text-xs">Headers (Optional)</Label>
<KeyValueEditor
value={headers}
onChange={setHeaders}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addButtonText="Add Header"
minRows={0}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<div />
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
<Button size="sm" onClick={handleSave} disabled={isLoading}>
{isLoading ? (
<>
<i className="i-mgc-loading-3-cute-re mr-2 size-4 animate-spin" />
Saving...
</>
) : (
"Save"
)}
</Button>
</div>
</div>
</div>
)
}
export const PanelStyleSegment = () => {
const { t } = useTranslation("ai")
const panelStyle = useAIChatPanelStyle()

View File

@ -0,0 +1,52 @@
import type { MCPService } from "@follow/shared/settings/interface"
import { followApi } from "~/lib/api-client"
// MCP Service API calls - these connect to the actual server endpoints via followClient
export const createMCPConnection = async (connectionData: {
name: string
transportType: "streamable-http" | "sse"
url: string
headers?: Record<string, string>
}): Promise<{ authorizationUrl?: string }> => {
const res = await followApi.mcp.createConnection(connectionData)
return { authorizationUrl: res.authorizationUrl }
}
export const fetchMCPConnections = async (): Promise<MCPService[]> => {
const response = await followApi.mcp.getConnections()
return response.data
}
export const updateMCPConnection = async (
connectionId: string,
updateData: {
name?: string
transportType?: "streamable-http" | "sse"
url?: string
headers?: Record<string, string>
},
): Promise<{ authorizationUrl?: string }> => {
const res = await followApi.mcp.updateConnection({ connectionId, ...updateData })
return { authorizationUrl: res.authorizationUrl }
}
export const deleteMCPConnection = async (connectionId: string): Promise<void> => {
await followApi.mcp.deleteConnection({ connectionId })
}
export const refreshMCPTools = async (connectionIds?: string[]): Promise<void> => {
await followApi.mcp.refreshTools({ connectionIds })
}
export const getMCPTools = async (connectionId: string) => {
const response = await followApi.mcp.getTools({ connectionId })
return response.data
}
// Query key factory for MCP queries
export const mcpQueryKeys = {
all: ["mcp"] as const,
connections: () => [...mcpQueryKeys.all, "connections"] as const,
tools: (connectionId: string) => [...mcpQueryKeys.all, "tools", connectionId] as const,
}

View File

@ -2,9 +2,6 @@ Rewrite:
Test url:
https://css-spring-generator.vercel.app/share/feeds/1
https://css-spring-generator.vercel.app/og/feed/1
http://localhost:2234/share/feeds/41223694984583197
http://localhost:2234/share/feeds/41375451836487680?view=2
http://localhost:2234/share/feeds/41147805276726317?view=2

View File

@ -65,7 +65,7 @@
"masonic": "4.1.0",
"nanoid": "5.1.5",
"path-to-regexp": "8.2.0",
"tailwindcss-uikit-colors": "1.0.0-alpha.1",
"tailwindcss-uikit-colors": "catalog:",
"tsdown": "0.12.9",
"tsx": "4.20.3",
"typescript": "catalog:",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M10.002 2.041c-1.207.168-2.251.992-2.719 2.146-.184.453-.223.672-.223 1.263V6h-.64c-1.033.001-1.569.111-2.26.464A3.978 3.978 0 0 0 2.464 8.16c-.392.765-.475 1.244-.456 2.6.015 1.053.029 1.116.297 1.378.368.358.734.409 1.326.187.235-.089.401-.125.569-.125.81 0 1.429.756 1.266 1.546-.126.612-.581 1.005-1.206 1.044-.239.015-.328 0-.612-.104-.414-.152-.629-.185-.837-.129-.25.067-.534.28-.67.5l-.121.197-.012 1.464c-.016 1.945.035 2.3.456 3.122a3.978 3.978 0 0 0 1.696 1.696c.82.42 1.18.473 3.099.456 1.609-.013 1.6-.012 1.88-.297.348-.355.399-.721.182-1.326-.114-.319-.129-.405-.112-.639.044-.62.438-1.071 1.045-1.196.768-.158 1.48.392 1.537 1.188.017.244.003.324-.114.648-.217.6-.165.969.184 1.325.264.269.326.282 1.379.297 1.356.019 1.835-.064 2.6-.456a3.978 3.978 0 0 0 1.696-1.696c.353-.691.463-1.226.464-2.26v-.64l.55-.001c.591 0 .813-.039 1.263-.222a3.522 3.522 0 0 0 2.072-2.337c.121-.455.121-1.305 0-1.76a3.522 3.522 0 0 0-2.072-2.337c-.45-.183-.672-.222-1.263-.222L18 10.06v-.14c0-.077-.027-.313-.058-.525-.222-1.469-1.266-2.705-2.697-3.192A5.5 5.5 0 0 0 14.08 6h-.14v-.55c0-.591-.039-.81-.223-1.263-.412-1.015-1.306-1.808-2.329-2.066a4.505 4.505 0 0 0-1.386-.08m1.133 2.113c.293.145.562.416.709.715.08.161.094.253.095.591.001.395-.001.405-.189.803-.166.35-.19.435-.19.66.001.328.125.607.363.81.275.236.445.267 1.456.267.959 0 1.166.03 1.551.225.265.135.708.575.84.835.185.366.201.476.226 1.58.023.982.031 1.072.111 1.22.196.365.55.578.965.579.215.001.309-.026.655-.184.222-.102.495-.197.608-.211a1.474 1.474 0 0 1 1.514.828c.119.241.131.3.131.628s-.012.387-.131.628c-.148.3-.416.568-.718.716-.161.08-.253.094-.591.095-.394.001-.406-.002-.808-.189-.352-.164-.444-.19-.66-.19a1.034 1.034 0 0 0-.805.363c-.253.295-.267.399-.267 1.948 0 1.493-.017 1.649-.229 2.066-.133.263-.575.703-.841.838-.277.14-.615.225-.898.225H13.8v-.256c0-1.395-.971-2.674-2.361-3.11-.262-.082-.379-.094-.939-.094-.559 0-.678.012-.94.094-.797.248-1.527.822-1.922 1.51-.261.454-.438 1.101-.438 1.6V20h-.732c-1.052 0-1.381-.105-1.872-.596-.491-.491-.596-.82-.596-1.872V16.8l.27-.001c.338-.001.884-.12 1.242-.27.975-.41 1.744-1.381 1.929-2.434.073-.414.032-1.197-.081-1.555-.245-.78-.824-1.511-1.504-1.902-.454-.261-1.101-.438-1.6-.438H4v-.232c0-.795.529-1.542 1.316-1.856.215-.086.287-.091 1.804-.111 1.523-.02 1.586-.024 1.74-.107.52-.282.717-.859.482-1.414-.333-.785-.312-.71-.293-1.077.02-.397.109-.618.359-.902.305-.348.686-.507 1.159-.487.249.01.358.037.568.14" fill="#10161F" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -13,6 +13,47 @@
"export_error": "Failed to export chat",
"export_success": "Chat exported successfully",
"features.title": "Features",
"integration.mcp.description": "Connect to MCP-compatible services that extend AI capabilities through secure OAuth integration",
"integration.mcp.enabled": "Enable MCP Services",
"integration.mcp.service.active": "Active",
"integration.mcp.service.added": "MCP service added successfully",
"integration.mcp.service.auth_message": "This service requires OAuth authorization. Click 'Open Authorization' to proceed with authentication in a new window.",
"integration.mcp.service.auth_required": "Authorization Required",
"integration.mcp.service.auth_window_opened": "Authorization window opened. Please complete authentication.",
"integration.mcp.service.baseUrl": "Service Base URL",
"integration.mcp.service.baseUrl_placeholder": "e.g. https://api.example.com",
"integration.mcp.service.connect": "Connect",
"integration.mcp.service.connected": "Connected",
"integration.mcp.service.connected_success": "Successfully connected to MCP service",
"integration.mcp.service.connecting": "Connecting...",
"integration.mcp.service.connection_failed": "Failed to connect to MCP service",
"integration.mcp.service.degraded": "Degraded",
"integration.mcp.service.deleted": "MCP service deleted successfully",
"integration.mcp.service.disconnect": "Disconnect",
"integration.mcp.service.disconnected": "Disconnected",
"integration.mcp.service.discover": "Discover Service",
"integration.mcp.service.discovery_failed": "Failed to discover service endpoints",
"integration.mcp.service.endpoints": "OAuth Endpoints",
"integration.mcp.service.error": "Connection Error",
"integration.mcp.service.healthy": "Healthy",
"integration.mcp.service.inactive": "Inactive",
"integration.mcp.service.name": "Service Name",
"integration.mcp.service.name_placeholder": "e.g. GitHub Tools, Slack Integration",
"integration.mcp.service.open_auth": "Open Authorization",
"integration.mcp.service.open_manually": "Open Authorization",
"integration.mcp.service.popup_blocked": "Authorization popup was blocked by your browser",
"integration.mcp.service.reconnect": "Reconnect",
"integration.mcp.service.scopes": "Required Scopes",
"integration.mcp.service.unhealthy": "Unhealthy",
"integration.mcp.service.updated": "MCP service updated successfully",
"integration.mcp.service.validation.baseUrl_required": "Base URL is required",
"integration.mcp.service.validation.invalid_url": "Please enter a valid URL",
"integration.mcp.service.validation.name_required": "Service name is required",
"integration.mcp.services.add": "Add Service",
"integration.mcp.services.empty.description": "Connect MCP services to extend AI capabilities with external tools, APIs, and data sources.",
"integration.mcp.services.empty.title": "No MCP Services Connected",
"integration.mcp.services.title": "MCP Services",
"integration.title": "Integration",
"personalize.description": "Tell me about yourself to get personalized AI responses",
"personalize.prompt.help": "This helps AI provide personalized responses based on your preferences.",
"personalize.prompt.label": "Personal Prompt",
@ -35,7 +76,6 @@
"shortcuts.added": "Shortcut added successfully",
"shortcuts.create_first": "Create your first shortcut",
"shortcuts.deleted": "Shortcut deleted successfully",
"shortcuts.description": "Create custom AI shortcuts for quick actions",
"shortcuts.empty.description": "Create custom AI shortcuts to quickly perform common tasks and get instant AI assistance.",
"shortcuts.empty.title": "No Shortcuts Yet",
"shortcuts.enabled": "Enabled",

View File

@ -13,6 +13,47 @@
"export_error": "Chat のエクスポートに失敗しました",
"export_success": "Chatが正常にエクスポートされました",
"features.title": "機能",
"integration.mcp.description": "安全なOAuth統合により、AI機能を拡張するMCP対応サービスに接続",
"integration.mcp.enabled": "MCPサービスを有効にする",
"integration.mcp.service.active": "アクティブ",
"integration.mcp.service.added": "MCPサービスが正常に追加されました",
"integration.mcp.service.auth_message": "このサービスはOAuth認証が必要です。新しいウィンドウで認証を進めるには「認証を開く」をクリックしてください。",
"integration.mcp.service.auth_required": "認証が必要",
"integration.mcp.service.auth_window_opened": "認証ウィンドウが開きました。認証を完了してください。",
"integration.mcp.service.baseUrl": "サービスベースURL",
"integration.mcp.service.baseUrl_placeholder": "例https://api.example.com",
"integration.mcp.service.connect": "接続",
"integration.mcp.service.connected": "接続済み",
"integration.mcp.service.connected_success": "MCPサービスに正常に接続されました",
"integration.mcp.service.connecting": "接続中...",
"integration.mcp.service.connection_failed": "MCPサービスへの接続に失敗しました",
"integration.mcp.service.degraded": "劣化",
"integration.mcp.service.deleted": "MCPサービスが正常に削除されました",
"integration.mcp.service.disconnect": "切断",
"integration.mcp.service.disconnected": "切断済み",
"integration.mcp.service.discover": "サービスを発見",
"integration.mcp.service.discovery_failed": "サービスエンドポイントの発見に失敗しました",
"integration.mcp.service.endpoints": "OAuthエンドポイント",
"integration.mcp.service.error": "接続エラー",
"integration.mcp.service.healthy": "正常",
"integration.mcp.service.inactive": "非アクティブ",
"integration.mcp.service.name": "サービス名",
"integration.mcp.service.name_placeholder": "例GitHub Tools、Slack Integration",
"integration.mcp.service.open_auth": "認証を開く",
"integration.mcp.service.open_manually": "認証を開く",
"integration.mcp.service.popup_blocked": "認証ポップアップがブラウザによってブロックされました",
"integration.mcp.service.reconnect": "再接続",
"integration.mcp.service.scopes": "必要なスコープ",
"integration.mcp.service.unhealthy": "異常",
"integration.mcp.service.updated": "MCPサービスが正常に更新されました",
"integration.mcp.service.validation.baseUrl_required": "ベースURLは必須です",
"integration.mcp.service.validation.invalid_url": "有効なURLを入力してください",
"integration.mcp.service.validation.name_required": "サービス名は必須です",
"integration.mcp.services.add": "サービスを追加",
"integration.mcp.services.empty.description": "MCPサービスに接続して、外部ツール、API、データソースでAI機能を拡張します。",
"integration.mcp.services.empty.title": "MCPサービスが接続されていません",
"integration.mcp.services.title": "MCPサービス",
"integration.title": "統合",
"personalize.description": "あなた自身について教えてください。そうすれば、パーソナライズされたAIの応答が得られます。",
"personalize.prompt.help": "これにより、AIはあなたの好みに基づいてパーソナライズされた応答を提供できます。",
"personalize.prompt.label": "個人用プロンプト",
@ -35,7 +76,6 @@
"shortcuts.added": "ショートカットが正常に追加されました",
"shortcuts.create_first": "最初のショートカットを作成",
"shortcuts.deleted": "ショートカットが正常に削除されました",
"shortcuts.description": "クイックアクションのためのカスタムAIショートカットを作成します",
"shortcuts.empty.description": "一般的なタスクを迅速に実行し、即座にAIアシスタンスを得るためのカスタムAIショートカットを作成します。",
"shortcuts.empty.title": "ショートカットはまだありません",
"shortcuts.enabled": "有効",

View File

@ -35,7 +35,6 @@
"shortcuts.added": "快捷方式添加成功",
"shortcuts.create_first": "创建您的第一个快捷方式",
"shortcuts.deleted": "快捷方式删除成功",
"shortcuts.description": "创建自定义AI快捷方式以快速执行操作",
"shortcuts.empty.description": "创建自定义AI快捷方式快速执行常见任务并获得即时AI协助。",
"shortcuts.empty.title": "暂无快捷方式",
"shortcuts.enabled": "已启用",

View File

@ -73,5 +73,12 @@
"14002": "Upload failed",
"15000": "AI token limit exceeded. Please try again later.",
"15001": "You are not allowed to access AI chat",
"undefined": "MCP configuration error"
"16000": "MCP OAuth error",
"16001": "MCP discovery error",
"16002": "MCP service not found",
"16003": "Invalid or expired OAuth state",
"16004": "Token exchange failed",
"16005": "User MCP service not found",
"16006": "MCP configuration error",
"16007": "MCP tool execution failed"
}

View File

@ -24,7 +24,7 @@
"tailwindcss-motion": "1.1.1",
"tailwindcss-multi": "0.4.6",
"tailwindcss-safe-area": "0.6.0",
"tailwindcss-uikit-colors": "1.0.0-alpha.1"
"tailwindcss-uikit-colors": "catalog:"
},
"devDependencies": {
"postcss": "8.5.6",

View File

@ -86,7 +86,7 @@
"remark-gh-alerts": "0.0.3",
"remark-rehype": "11.1.2",
"sonner": "2.0.6",
"tailwindcss-uikit-colors": "1.0.0-alpha.1",
"tailwindcss-uikit-colors": "catalog:",
"unified": "11.0.5",
"unist-util-visit": "5.0.0",
"unist-util-visit-parents": "5.1.3",

View File

@ -96,7 +96,7 @@ interface Token {
function highlightJson(jsonString: string): string {
// Escape HTML entities first
const escaped = jsonString
.replaceAll("&", "&amp;")
?.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")

View File

@ -156,6 +156,10 @@ export const defaultAISettings: AISettings = {
personalizePrompt: "",
shortcuts: [],
// MCP Services
mcpEnabled: false,
mcpServices: [],
// Features
autoScrollWhenStreaming: true,
}

View File

@ -178,10 +178,31 @@ export interface AIShortcut {
hotkey?: string
}
export type MCPTransportType = "streamable-http" | "sse"
export interface MCPService {
id: string
name: string
transportType: MCPTransportType
url?: string
headers?: Record<string, string>
isConnected: boolean
lastError?: string
toolCount: number
resourceCount: number
promptCount: number
createdAt: string
lastUsed: string | null
}
export interface AISettings {
personalizePrompt: string
shortcuts: AIShortcut[]
// MCP Services (stored locally, actual connections managed via server API)
mcpEnabled: boolean
mcpServices: MCPService[]
// Features
autoScrollWhenStreaming: boolean
}

View File

@ -7,8 +7,11 @@ settings:
catalogs:
default:
'@follow-app/client-sdk':
specifier: 0.3.29
version: 0.3.29
specifier: 0.3.38
version: 0.3.38
tailwindcss-uikit-colors:
specifier: 1.0.0
version: 1.0.0
typescript:
specifier: 5.8.3
version: 5.8.3
@ -462,7 +465,7 @@ importers:
version: 3.0.2(electron@37.2.0)
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
version: 0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@follow/database':
specifier: workspace:*
version: link:../../../../packages/internal/database
@ -1330,8 +1333,8 @@ importers:
specifier: 8.2.0
version: 8.2.0
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
version: 1.0.0-alpha.1
specifier: 'catalog:'
version: 1.0.0
tsdown:
specifier: 0.12.9
version: 0.12.9(typescript@5.8.3)
@ -1409,8 +1412,8 @@ importers:
specifier: 0.6.0
version: 0.6.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.10)(typescript@5.8.3)))
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
version: 1.0.0-alpha.1
specifier: 'catalog:'
version: 1.0.0
devDependencies:
postcss:
specifier: 8.5.6
@ -1635,8 +1638,8 @@ importers:
specifier: 2.0.6
version: 2.0.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
tailwindcss-uikit-colors:
specifier: 1.0.0-alpha.1
version: 1.0.0-alpha.1
specifier: 'catalog:'
version: 1.0.0
unified:
specifier: 11.0.5
version: 11.0.5
@ -1670,7 +1673,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
version: 0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@ -1682,7 +1685,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
version: 0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@ -1760,7 +1763,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
version: 0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@follow/constants':
specifier: workspace:*
version: link:../constants
@ -1824,7 +1827,7 @@ importers:
dependencies:
'@follow-app/client-sdk':
specifier: 'catalog:'
version: 0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
version: 0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@follow/configs':
specifier: workspace:*
version: link:../../configs
@ -4048,23 +4051,29 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@follow-app/client-sdk@0.3.29':
resolution: {integrity: sha512-bQgY0S6xrWO0iLO4eeptU4vxcmQ36iSrLl6omNscK6SCXzlebCYRmqQy7dsFHU4+bditADjjLulCGWmbDX9IJQ==}
'@follow-app/client-sdk@0.3.38':
resolution: {integrity: sha512-dewSXei/nLfwY+S6a4P8B9xlAgh4LhORyBWphN87b54Lbs3kX3ydx6esrcXliUTwOFH+TkpxbIQR16zPR/ApBw==}
'@folo-services/ai-tools@0.2.20':
resolution: {integrity: sha512-1AJI48H2g64JhuTruSqQKUrn4Tc353hMFdKAbohh65AxTAMN/1pIw9w17Dtjm+g5BVlZUXYKaiIvaN9tHsXLNA==}
'@folo-services/constants@0.1.18':
resolution: {integrity: sha512-3LDxX4Ebvr6GLdL5Ce2ZkAJwfNjfsdqOLpuFRZ3bq2gVwQZbIGsln+qYnInmR5+1i3DtebglrAj/IktnT3BsIg==}
'@folo-services/constants@0.1.21':
resolution: {integrity: sha512-4ws5nXxYjvm204rHH03ohUAhll5oWXgvlRnVGzxe9BkW+f64kJOshFxEeTxMDm2SMj0cnjEblsGvA29xJfXs/A==}
'@folo-services/drizzle@0.1.13':
resolution: {integrity: sha512-h6tndGnTLrAmfvxK5XsXGdZaMi2GBmFZBFiBqtabC7Y05A1ZSOaWg/d9f60Ydh586LmVonz5VGrhvzTdn5ukhg==}
'@folo-services/drizzle@0.1.16':
resolution: {integrity: sha512-ieo0vAf3KRlz6GZ9WY2BRut80qB9nxplTHS8rRtUJVnrsqP6mXDgChjAoX32vy3YKHk6xJfrzgWLoAlqfsvxyQ==}
'@folo-services/exceptions@0.1.11':
resolution: {integrity: sha512-OMKat8KcIxjw9pBfG3pC1kRcIs0YiuaYmMDA2MRy8AEGcki8F9u5e5QH7OKgP/LlNR7Fs2Csz1XetvcC4nB+RQ==}
'@folo-services/shared@0.0.8':
resolution: {integrity: sha512-rfl1vvGMxUnchckGSKOkeT5hS1NAzZTJ8gPF6yFR+TuSe2SZzq1a10/66nK9OxmqaDFaHdOpQfNXz2vp0gRJpg==}
'@folo-services/exceptions@0.1.14':
resolution: {integrity: sha512-VFVRkzXg7BytEvdg7iyUXc6qaAudTTtOU8PrTkRUG4ZB9udLE/uVT5q+6poS1CkwMi2GroH27AFC4fnfVp6Fvw==}
'@folo-services/shared@0.0.11':
resolution: {integrity: sha512-WS0OVoswUfPr4X8SCPYdyxx4XL7Khh63NCk0pilCtci3XtO2kMPWlLEM4gJAfg6g5GAs4oVNvsJiNEKEa3Lo1g==}
'@fontsource/sn-pro@5.2.5':
resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==}
@ -7119,6 +7128,9 @@ packages:
apple-uikit-colors@0.6.2:
resolution: {integrity: sha512-sv0b92krbTZGNt4AJQLAPioUvuEI/wW2cLqGuEBWl3KpU2JK/odOftx8+dGSC5jGWhdB5qnwBqF96X2FA8FUQA==}
apple-uikit-colors@1.0.0:
resolution: {integrity: sha512-G2Ti2ogMOOC1phfHacSrLacDiE0RcLI7IG3aNoaw7Ack23WI8/9QczmUuhxxkU9BoIR2euCZ2G4lD5qJ+OknwA==}
archiver-utils@2.1.0:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
engines: {node: '>= 6'}
@ -14571,8 +14583,8 @@ packages:
tailwindcss-uikit-colors@0.6.2:
resolution: {integrity: sha512-RfF3VVB2nHvJwPZ2ZSTMgKYbZjRny/KW3rP6ufg6/bopxINDQ9TpN/fJXxvKnD86hIU0F2oLIfAv2r6o/ZWcZw==}
tailwindcss-uikit-colors@1.0.0-alpha.1:
resolution: {integrity: sha512-akeDIVZMRO6tFbr6x4YsRkx24xx/ofEfsJdL2lq7AE/sDBnUbs5Pr8by4ngBbxJ2U2eam000B/eJrb6r6YzLmw==}
tailwindcss-uikit-colors@1.0.0:
resolution: {integrity: sha512-18MGdMVSoXFKjcVkUIu7Q5USqdenAvgFJT9DEocPmdS94b8d/z3s0Uw8LvaNDINfDN9ls24hfg6/hyFqpcZpxw==}
tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
@ -18921,12 +18933,12 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@follow-app/client-sdk@0.3.29(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
'@follow-app/client-sdk@0.3.38(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
dependencies:
'@folo-services/constants': 0.1.18
'@folo-services/drizzle': 0.1.13(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
'@folo-services/exceptions': 0.1.11
'@folo-services/shared': 0.0.8(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
'@folo-services/constants': 0.1.21
'@folo-services/drizzle': 0.1.16(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
'@folo-services/exceptions': 0.1.14
'@folo-services/shared': 0.0.11(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
zod: 3.25.76
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
@ -18999,7 +19011,7 @@ snapshots:
- sql.js
- sqlite3
'@folo-services/constants@0.1.18':
'@folo-services/constants@0.1.21':
dependencies:
zod: 3.25.76
@ -19042,11 +19054,52 @@ snapshots:
- sql.js
- sqlite3
'@folo-services/drizzle@0.1.16(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)':
dependencies:
'@folo-services/exceptions': 0.1.14
drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76)
nanoid: 5.1.5
pg: 8.16.3
zod: 3.25.76
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
- '@electric-sql/pglite'
- '@libsql/client'
- '@libsql/client-wasm'
- '@neondatabase/serverless'
- '@op-engineering/op-sqlite'
- '@opentelemetry/api'
- '@planetscale/database'
- '@prisma/client'
- '@tidbcloud/serverless'
- '@types/better-sqlite3'
- '@types/pg'
- '@types/sql.js'
- '@upstash/redis'
- '@vercel/postgres'
- '@xata.io/client'
- better-sqlite3
- bun-types
- expo-sqlite
- gel
- knex
- kysely
- mysql2
- pg-native
- postgres
- prisma
- sql.js
- sqlite3
'@folo-services/exceptions@0.1.11': {}
'@folo-services/shared@0.0.8(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
'@folo-services/exceptions@0.1.14': {}
'@folo-services/shared@0.0.11(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)':
dependencies:
'@folo-services/drizzle': 0.1.13(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
'@folo-services/drizzle': 0.1.16(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)
drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)
drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76)
zod: 3.25.76
@ -22755,6 +22808,8 @@ snapshots:
apple-uikit-colors@0.6.2: {}
apple-uikit-colors@1.0.0: {}
archiver-utils@2.1.0:
dependencies:
glob: 7.2.3
@ -31549,9 +31604,9 @@ snapshots:
dependencies:
apple-uikit-colors: 0.6.2
tailwindcss-uikit-colors@1.0.0-alpha.1:
tailwindcss-uikit-colors@1.0.0:
dependencies:
apple-uikit-colors: 0.6.2
apple-uikit-colors: 1.0.0
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.10)(typescript@5.8.3)):
dependencies:

View File

@ -58,4 +58,5 @@ overrides:
catalog:
typescript: "5.8.3"
"@follow-app/client-sdk": "0.3.29"
"@follow-app/client-sdk": "0.3.38"
tailwindcss-uikit-colors: "1.0.0"