@@ -31,10 +31,7 @@ import {
3131
3232import { JsonImportModal } from "./connection/JsonImportModal" ;
3333import { ServerFormData } from "@/shared/types.js" ;
34- import {
35- useAppReady ,
36- useAppReadyMessage ,
37- } from "@/hooks/use-app-ready" ;
34+ import { useAppReady , useAppReadyMessage } from "@/hooks/use-app-ready" ;
3835import { MCPIcon } from "./ui/mcp-icon" ;
3936import { usePostHog } from "posthog-js/react" ;
4037import {
@@ -89,7 +86,9 @@ import {
8986import {
9087 useProjectServers as useRemoteProjectServers ,
9188 useProjectMembers ,
89+ useServerMutations ,
9290 shouldQueryProjectId ,
91+ type RemoteServer ,
9392} from "@/hooks/useProjects" ;
9493import { projectClientCapabilitiesNeedReconnect } from "@/lib/client-config" ;
9594import {
@@ -260,10 +259,7 @@ function writePersistedLoggerFocus(input: PersistedLoggerFocus): void {
260259 }
261260
262261 try {
263- sessionStorage . setItem (
264- LOGGER_FOCUS_STORAGE_KEY ,
265- JSON . stringify ( input )
266- ) ;
262+ sessionStorage . setItem ( LOGGER_FOCUS_STORAGE_KEY , JSON . stringify ( input ) ) ;
267263 } catch {
268264 // ignore
269265 }
@@ -428,6 +424,9 @@ function SortableServerCard({
428424 hostedServerId,
429425 onOpenDetailModal,
430426 projectId,
427+ moveTargets,
428+ onMoveToProject,
429+ isMovingToProject,
431430} : {
432431 id : string ;
433432 dndDisabled : boolean ;
@@ -448,6 +447,12 @@ function SortableServerCard({
448447 defaultTab : ServerDetailTab
449448 ) => void ;
450449 projectId : string ;
450+ moveTargets ?: Array < { id : string ; name : string ; icon ?: string } > ;
451+ onMoveToProject ?: (
452+ serverName : string ,
453+ targetProjectId : string
454+ ) => void | Promise < void > ;
455+ isMovingToProject ?: boolean ;
451456} ) {
452457 const { listeners, setNodeRef, transform, transition, isDragging } =
453458 useSortable ( { id, disabled : dndDisabled } ) ;
@@ -479,6 +484,9 @@ function SortableServerCard({
479484 hostedServerId = { hostedServerId }
480485 onOpenDetailModal = { onOpenDetailModal }
481486 projectId = { projectId }
487+ moveTargets = { moveTargets }
488+ onMoveToProject = { onMoveToProject }
489+ isMovingToProject = { isMovingToProject }
482490 />
483491 ) ;
484492
@@ -536,10 +544,7 @@ interface ServersTabProps {
536544 isLoadingProjects ?: boolean ;
537545 isAuthHydrating ?: boolean ;
538546 areServersHydrated ?: boolean ;
539- onProjectShared ?: (
540- sharedProjectId : string ,
541- sourceProjectId ?: string
542- ) => void ;
547+ onProjectShared ?: ( sharedProjectId : string , sourceProjectId ?: string ) => void ;
543548 onLeaveProject ?: ( ) => void ;
544549 isRegistryEnabled ?: boolean ;
545550 onNavigateToRegistry ?: ( ) => void ;
@@ -584,7 +589,7 @@ export function ServersTab({
584589 const sharedProjectIdForHostScope =
585590 projects [ activeProjectId ] ?. sharedProjectId ?? null ;
586591 const [ previewedHostId ] = usePreviewedHostId (
587- sharedProjectIdForHostScope ?? activeProjectId ?? null ,
592+ sharedProjectIdForHostScope ?? activeProjectId ?? null
588593 ) ;
589594 const { host : previewedHost } = useHost ( {
590595 isAuthenticated,
@@ -595,6 +600,77 @@ export function ServersTab({
595600 isAuthenticated,
596601 } ) ;
597602
603+ // --- Move a server to another project (kebab menu) ---
604+ // Server-side move keeps encrypted secret material private and avoids
605+ // reporting success before the source server is deleted.
606+ const { moveServerToProject } = useServerMutations ( ) ;
607+ const [ movingServerName , setMovingServerName ] = useState < string | null > ( null ) ;
608+
609+ const remoteServersByName = useMemo ( ( ) => {
610+ const map : Record < string , RemoteServer > = { } ;
611+ for ( const s of viewProjectServersList ?? [ ] ) map [ s . name ] = s ;
612+ return map ;
613+ } , [ viewProjectServersList ] ) ;
614+
615+ const moveTargets = useMemo (
616+ ( ) =>
617+ Object . values ( projects )
618+ . filter ( ( p ) => p . id !== activeProjectId && ! ! p . sharedProjectId )
619+ . filter ( ( p ) =>
620+ organizationId
621+ ? p . organizationId === organizationId
622+ : ! p . organizationId
623+ )
624+ . sort ( ( a , b ) => {
625+ if ( a . isDefault ) return - 1 ;
626+ if ( b . isDefault ) return 1 ;
627+ return a . name . localeCompare ( b . name ) ;
628+ } )
629+ . map ( ( p ) => ( { id : p . id , name : p . name , icon : p . icon } ) ) ,
630+ [ projects , activeProjectId , organizationId ]
631+ ) ;
632+
633+ const handleMoveServerToProject = useCallback (
634+ async ( serverName : string , targetProjectId : string ) => {
635+ const remote = remoteServersByName [ serverName ] ;
636+ const target = projects [ targetProjectId ] ;
637+ const targetSharedId = target ?. sharedProjectId ;
638+ if ( ! remote ) {
639+ toast . error ( `Couldn't find "${ serverName } " to move.` ) ;
640+ return ;
641+ }
642+ if ( ! targetSharedId ) {
643+ toast . error ( `"${ target ?. name ?? "That project" } " isn't synced yet.` ) ;
644+ return ;
645+ }
646+ setMovingServerName ( serverName ) ;
647+ try {
648+ await moveServerToProject ( {
649+ serverId : remote . _id ,
650+ targetProjectId : targetSharedId ,
651+ } ) ;
652+ await Promise . resolve ( onDisconnect ( serverName ) ) ;
653+ await Promise . resolve ( onRemove ( serverName ) ) ;
654+ toast . success ( `Moved "${ serverName } " to ${ target . name } ` ) ;
655+ } catch ( error ) {
656+ // Keep the raw error in the console for us; users get a friendly toast.
657+ console . error (
658+ `Failed to move server "${ serverName } " to "${ target . name } ":` ,
659+ error
660+ ) ;
661+ const message = error instanceof Error ? error . message : String ( error ) ;
662+ toast . error (
663+ / a l r e a d y e x i s t s | a l r e a d y h a s / i. test ( message )
664+ ? `"${ target . name } " already has a server named "${ serverName } ". Rename or remove it there first.`
665+ : `Couldn't move "${ serverName } " to "${ target . name } ". Please try again.`
666+ ) ;
667+ } finally {
668+ setMovingServerName ( null ) ;
669+ }
670+ } ,
671+ [ remoteServersByName , projects , moveServerToProject , onDisconnect , onRemove ]
672+ ) ;
673+
598674 // Project-wide auto-connect toggle. Single switch in the header that
599675 // either enrolls every catalog server in project.serverIds (ON) or
600676 // clears the set (OFF). Overrides on still-included servers are
@@ -621,18 +697,18 @@ export function ServersTab({
621697 "projectServerConfig:getConfig" as any ,
622698 sharedProjectIdForHostScope && isAuthenticated
623699 ? ( { projectId : sharedProjectIdForHostScope } as any )
624- : "skip" ,
700+ : "skip"
625701 ) as ProjectServerConfigDto | null | undefined ;
626702 const setProjectServerConfigMutation = useMutation (
627- "projectServerConfig:setConfig" as any ,
703+ "projectServerConfig:setConfig" as any
628704 ) as unknown as ( args : {
629705 projectId : string ;
630706 input : ProjectServerConfigInput ;
631707 } ) => Promise < ProjectServerConfigDto > ;
632708 const [ isTogglingAutoConnect , setIsTogglingAutoConnect ] = useState ( false ) ;
633709 const catalogServerIds = useMemo (
634710 ( ) => ( viewProjectServersList ?? [ ] ) . map ( ( s ) => s . _id ) ,
635- [ viewProjectServersList ] ,
711+ [ viewProjectServersList ]
636712 ) ;
637713 const autoConnectAll = useMemo ( ( ) => {
638714 if ( ! projectServerConfigDto || catalogServerIds . length === 0 ) return false ;
@@ -656,8 +732,8 @@ export function ServersTab({
656732 const catalogIdSet = new Set ( catalogServerIds ) ;
657733 const preservedOverrides = Object . fromEntries (
658734 Object . entries ( projectServerConfigDto ?. overrides ?? { } ) . filter (
659- ( [ id ] ) => catalogIdSet . has ( id ) ,
660- ) ,
735+ ( [ id ] ) => catalogIdSet . has ( id )
736+ )
661737 ) ;
662738 await setProjectServerConfigMutation ( {
663739 projectId : sharedProjectIdForHostScope ,
@@ -688,7 +764,7 @@ export function ServersTab({
688764 catalogServerIds ,
689765 projectServerConfigDto ,
690766 setProjectServerConfigMutation ,
691- ] ,
767+ ]
692768 ) ;
693769
694770 const renderAutoConnectToggle = ( ) => {
@@ -703,7 +779,7 @@ export function ServersTab({
703779 < label
704780 className = { cn (
705781 "flex items-center gap-2 text-xs text-muted-foreground select-none" ,
706- disabled ? "cursor-not-allowed" : "cursor-pointer" ,
782+ disabled ? "cursor-not-allowed" : "cursor-pointer"
707783 ) }
708784 title = {
709785 canManageProjectServers
@@ -726,7 +802,7 @@ export function ServersTab({
726802 const requiredIds = previewedHost ?. config ?. serverIds ?? [ ] ;
727803 if ( requiredIds . length === 0 || ! viewProjectServersList ) return [ ] ;
728804 const byId = new Map (
729- viewProjectServersList . map ( ( s ) => [ s . _id , s . name ] as const ) ,
805+ viewProjectServersList . map ( ( s ) => [ s . _id , s . name ] as const )
730806 ) ;
731807 return requiredIds
732808 . map ( ( id ) => byId . get ( id ) )
@@ -783,9 +859,7 @@ export function ServersTab({
783859 const persistedLoggerFocus = readPersistedLoggerFocus ( activeProjectId ) ;
784860 const [ focusedLoggerServerIds , setFocusedLoggerServerIds ] = useState <
785861 string [ ] | undefined
786- > (
787- persistedLoggerFocus ? [ persistedLoggerFocus . serverName ] : undefined
788- ) ;
862+ > ( persistedLoggerFocus ? [ persistedLoggerFocus . serverName ] : undefined ) ;
789863 const [ focusedLoggerSinceTimestamp , setFocusedLoggerSinceTimestamp ] =
790864 useState < number | undefined > ( persistedLoggerFocus ?. sinceTimestamp ) ;
791865 const [ detailModalState , setDetailModalState ] = useState < {
@@ -803,10 +877,7 @@ export function ServersTab({
803877 } ) ;
804878
805879 // --- Self-contained local ordering (localStorage only, never synced to Convex) ---
806- const allNames = useMemo (
807- ( ) => Object . keys ( projectServers ) ,
808- [ projectServers ]
809- ) ;
880+ const allNames = useMemo ( ( ) => Object . keys ( projectServers ) , [ projectServers ] ) ;
810881
811882 const [ orderedServerNames , setOrderedServerNames ] = useState < string [ ] > ( ( ) => {
812883 const saved = loadServerOrder ( activeProjectId ) ;
@@ -1063,11 +1134,12 @@ export function ServersTab({
10631134 const activeProject = projects [ activeProjectId ] ;
10641135 const sharedProjectId = activeProject ?. sharedProjectId ;
10651136 const hostedProjectId = sharedProjectId ?? activeProjectId ;
1066- const { serversRecord : sharedProjectServersRecord } =
1067- useRemoteProjectServers ( {
1137+ const { serversRecord : sharedProjectServersRecord } = useRemoteProjectServers (
1138+ {
10681139 projectId : sharedProjectId ?? null ,
10691140 isAuthenticated,
1070- } ) ;
1141+ }
1142+ ) ;
10711143 const detailModalHostedServerId = detailModalServer
10721144 ? sharedProjectServersRecord [ detailModalServer . name ] ?. _id
10731145 : undefined ;
@@ -1169,7 +1241,9 @@ export function ServersTab({
11691241
11701242 const handleJsonImport = ( servers : ServerFormData [ ] ) => {
11711243 if ( isAppBootstrapping ) {
1172- toast . error ( appReadyMessage ?? "App is still loading. Try again in a moment." ) ;
1244+ toast . error (
1245+ appReadyMessage ?? "App is still loading. Try again in a moment."
1246+ ) ;
11731247 return ;
11741248 }
11751249 servers . forEach ( ( server ) => {
@@ -1181,7 +1255,9 @@ export function ServersTab({
11811255 const handleConnectServer = useCallback (
11821256 ( formData : ServerFormData ) => {
11831257 if ( isAppBootstrapping ) {
1184- toast . error ( appReadyMessage ?? "App is still loading. Try again in a moment." ) ;
1258+ toast . error (
1259+ appReadyMessage ?? "App is still loading. Try again in a moment."
1260+ ) ;
11851261 return ;
11861262 }
11871263 focusLoggerOnServer ( formData . name ) ;
@@ -1196,10 +1272,12 @@ export function ServersTab({
11961272 options ?: {
11971273 forceOAuthFlow ?: boolean ;
11981274 allowInteractiveOAuthFlow ?: boolean ;
1199- } ,
1275+ }
12001276 ) => {
12011277 if ( isAppBootstrapping ) {
1202- toast . error ( appReadyMessage ?? "App is still loading. Try again in a moment." ) ;
1278+ toast . error (
1279+ appReadyMessage ?? "App is still loading. Try again in a moment."
1280+ ) ;
12031281 return ;
12041282 }
12051283 focusLoggerOnServer ( serverName ) ;
@@ -1221,7 +1299,9 @@ export function ServersTab({
12211299
12221300 const handleQuickConnect = async ( server : EnrichedRegistryServer ) => {
12231301 if ( isAppBootstrapping ) {
1224- toast . error ( appReadyMessage ?? "App is still loading. Try again in a moment." ) ;
1302+ toast . error (
1303+ appReadyMessage ?? "App is still loading. Try again in a moment."
1304+ ) ;
12251305 return ;
12261306 }
12271307 const serverName = getRegistryServerName ( server ) ;
@@ -1537,7 +1617,9 @@ export function ServersTab({
15371617 { /* Header Section */ }
15381618 < div className = "flex flex-wrap items-center justify-end gap-2" >
15391619 < div className = "flex items-center gap-2" >
1540- { showServerActionsInHostsHeader ? null : renderAutoConnectToggle ( ) }
1620+ { showServerActionsInHostsHeader
1621+ ? null
1622+ : renderAutoConnectToggle ( ) }
15411623 { shouldShowBrowseRegistryOnly && onNavigateToRegistry ? (
15421624 < Button
15431625 variant = "outline"
@@ -1550,7 +1632,9 @@ export function ServersTab({
15501632 < ArrowRight className = "h-3.5 w-3.5 ml-1" />
15511633 </ Button >
15521634 ) : null }
1553- { ! showServerActionsInHostsHeader ? renderServerActionsMenu ( ) : null }
1635+ { ! showServerActionsInHostsHeader
1636+ ? renderServerActionsMenu ( )
1637+ : null }
15541638 </ div >
15551639 </ div >
15561640
@@ -1592,6 +1676,9 @@ export function ServersTab({
15921676 hostedServerId = { sharedProjectServersRecord [ name ] ?. _id }
15931677 onOpenDetailModal = { handleOpenDetailModal }
15941678 projectId = { activeProjectId }
1679+ moveTargets = { moveTargets }
1680+ onMoveToProject = { handleMoveServerToProject }
1681+ isMovingToProject = { movingServerName === name }
15951682 />
15961683 ) ;
15971684 } ) }
@@ -1614,9 +1701,7 @@ export function ServersTab({
16141701 clearPendingQuickConnectIfMatches ( serverName ) ;
16151702 onRemove ( serverName ) ;
16161703 } }
1617- hostedServerId = {
1618- sharedProjectServersRecord [ activeId ! ] ?. _id
1619- }
1704+ hostedServerId = { sharedProjectServersRecord [ activeId ! ] ?. _id }
16201705 projectId = { activeProjectId }
16211706 />
16221707 </ div >
@@ -1756,19 +1841,15 @@ export function ServersTab({
17561841 { isAuthHydrating ||
17571842 isBillingContextPending ||
17581843 isLoadingProjects ||
1759- ! areServersHydrated ? (
1760- renderLoadingContent ( )
1761- ) : ! selectedProject ? (
1762- shouldQueryProjectId ( activeProjectId ) ? (
1763- renderLoadingContent ( )
1764- ) : (
1765- renderNoProjectContent ( )
1766- )
1767- ) : hasAnyServers ? (
1768- renderConnectedContent ( )
1769- ) : (
1770- renderEmptyContent ( )
1771- ) }
1844+ ! areServersHydrated
1845+ ? renderLoadingContent ( )
1846+ : ! selectedProject
1847+ ? shouldQueryProjectId ( activeProjectId )
1848+ ? renderLoadingContent ( )
1849+ : renderNoProjectContent ( )
1850+ : hasAnyServers
1851+ ? renderConnectedContent ( )
1852+ : renderEmptyContent ( ) }
17721853
17731854 { /* Add Server Modal */ }
17741855 < AddServerModal
@@ -1825,7 +1906,7 @@ export function ServersTab({
18251906 { renderAutoConnectToggle ( ) }
18261907 { renderServerActionsMenu ( ) }
18271908 </ > ,
1828- hostsConnectAddServerSlot ,
1909+ hostsConnectAddServerSlot
18291910 )
18301911 : null }
18311912 </ div >
0 commit comments