Skip to content

Commit 2bca2ab

Browse files
viktormarinhoclaude
andcommitted
fix(ui): replace client-side registry detection with server-side binding filter
Use useConnections({ binding: "REGISTRY" }) instead of loading all connections and filtering client-side with useRegistryConnections(). Also replace bare useConnections() for duplicate checking in add-connection-dialog with a direct MCP tool call, and use targeted searchTerm queries for publisher lookup in mcp-server-detail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bc89175 commit 2bca2ab

6 files changed

Lines changed: 66 additions & 44 deletions

File tree

apps/mesh/src/web/components/details/virtual-mcp/add-connection-dialog.tsx

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { CollectionTabs } from "@/web/components/collections/collection-tabs.tsx
33
import { ConnectionCard } from "@/web/components/connections/connection-card.tsx";
44
import { IntegrationIcon } from "@/web/components/integration-icon.tsx";
55
import type { RegistryItem } from "@/web/components/store/types";
6-
import { useRegistryConnections } from "@/web/hooks/use-binding";
76
import { useInfiniteScroll } from "@/web/hooks/use-infinite-scroll";
87
import { useLocalStorage } from "@/web/hooks/use-local-storage";
98
import { LOCALSTORAGE_KEYS } from "@/web/lib/localstorage-keys";
@@ -34,8 +33,10 @@ import {
3433
import { cn } from "@deco/ui/lib/utils.ts";
3534
import {
3635
type ConnectionEntity,
36+
SELF_MCP_ALIAS_ID,
3737
useConnectionActions,
3838
useConnections,
39+
useMCPClient,
3940
useProjectContext,
4041
} from "@decocms/mesh-sdk";
4142
import { useQueryClient } from "@tanstack/react-query";
@@ -174,22 +175,17 @@ function AddConnectionDialogContent({
174175
() => "all",
175176
);
176177

177-
// Connections
178-
const allConnections = useConnections();
178+
// Connections - use server-side search, exclude virtual
179+
const allConnections = useConnections({
180+
searchTerm: search || undefined,
181+
});
179182
const nonVirtual = allConnections.filter(
180183
(c) => c.connection_type !== "VIRTUAL",
181184
);
182-
const filteredConnections = search
183-
? nonVirtual.filter(
184-
(c) =>
185-
c.title.toLowerCase().includes(searchLower) ||
186-
c.description?.toLowerCase().includes(searchLower),
187-
)
188-
: nonVirtual;
189-
const grouped = groupConnections(filteredConnections);
190-
191-
// Registry / catalog
192-
const registryConnections = useRegistryConnections(allConnections).sort(
185+
const grouped = groupConnections(nonVirtual);
186+
187+
// Registry / catalog - use server-side binding filter
188+
const registryConnections = useConnections({ binding: "REGISTRY" }).sort(
193189
(a, b) => {
194190
const isSelfA = a.app_name === "@deco/management-mcp";
195191
const isSelfB = b.app_name === "@deco/management-mcp";
@@ -569,7 +565,10 @@ export function AddConnectionDialog({
569565
const { data: session } = authClient.useSession();
570566
const connectionActions = useConnectionActions();
571567
const queryClient = useQueryClient();
572-
const allConnections = useConnections();
568+
const selfClient = useMCPClient({
569+
connectionId: SELF_MCP_ALIAS_ID,
570+
orgId: org.id,
571+
});
573572

574573
const handleInlineConnect = async (item: RegistryItem) => {
575574
if (!org || !session?.user?.id) return;
@@ -599,14 +598,34 @@ export function AddConnectionDialog({
599598
return;
600599
}
601600

602-
// Check for duplicates
601+
// Check for duplicates via server-side search
603602
const appName = item.id ?? item.title;
604-
const existing = allConnections.filter(
605-
(c) => c.app_name === appName || c.title === connectionData.title,
606-
);
607-
if (existing.length > 0) {
603+
let existingCount = 0;
604+
try {
605+
const result = (await selfClient.callTool({
606+
name: "COLLECTION_CONNECTIONS_LIST",
607+
arguments: {
608+
where: {
609+
operator: "or",
610+
conditions: [
611+
{ field: ["app_name"], operator: "eq", value: appName },
612+
{
613+
field: ["title"],
614+
operator: "eq",
615+
value: connectionData.title,
616+
},
617+
],
618+
},
619+
limit: 100,
620+
},
621+
})) as { structuredContent?: { totalCount?: number } };
622+
existingCount = result.structuredContent?.totalCount ?? 0;
623+
} catch {
624+
// If the check fails, proceed without renaming
625+
}
626+
if (existingCount > 0) {
608627
const baseName = connectionData.title || "MCP Server";
609-
connectionData.title = `${baseName} (${existing.length + 1})`;
628+
connectionData.title = `${baseName} (${existingCount + 1})`;
610629
}
611630

612631
const { id } = await connectionActions.create.mutateAsync(connectionData);

apps/mesh/src/web/hooks/use-binding-schema-from-registry.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import { useSuspenseQueries } from "@tanstack/react-query";
1010
import type { BindingDefinition } from "@/web/hooks/use-binding";
11-
import { useRegistryConnections } from "@/web/hooks/use-binding";
1211
import { KEYS } from "@/web/lib/query-keys";
1312
import { MCP_REGISTRY_DECOCMS_KEY } from "@/web/utils/constants";
1413
import { findListToolName, callRegistryTool } from "@/web/utils/registry-utils";
@@ -102,9 +101,8 @@ export function useBindingSchemaFromRegistry(
102101
serverName: string | undefined,
103102
): UseBindingSchemaFromRegistryResult {
104103
const { org } = useProjectContext();
105-
// Get all connections and filter to registry connections
106-
const allConnections = useConnections();
107-
const registryConnections = useRegistryConnections(allConnections);
104+
// Get registry connections using server-side binding filter
105+
const registryConnections = useConnections({ binding: "REGISTRY" });
108106

109107
// Parse the MCP Server name for the query
110108
const parsedServerName = serverName ? parseServerName(serverName) : "";

apps/mesh/src/web/hooks/use-install-from-registry.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import { toast } from "sonner";
77
import type { RegistryItem } from "@/web/components/store/types";
8-
import { useRegistryConnections } from "@/web/hooks/use-binding";
98
import { authClient } from "@/web/lib/auth-client";
109
import {
1110
useConnectionActions,
@@ -56,9 +55,8 @@ export function useInstallFromRegistry(): UseInstallFromRegistryResult {
5655
const { data: session } = authClient.useSession();
5756
const actions = useConnectionActions();
5857

59-
// Get all connections and filter to registry connections
60-
const allConnections = useConnections();
61-
const registryConnections = useRegistryConnections(allConnections);
58+
// Get registry connections using server-side binding filter
59+
const registryConnections = useConnections({ binding: "REGISTRY" });
6260

6361
// Installation function - queries registries directly with MCP Server name filter
6462
const installByBinding = async (

apps/mesh/src/web/routes/orgs/connections.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ErrorBoundary } from "@/web/components/error-boundary";
88
import { IntegrationIcon } from "@/web/components/integration-icon.tsx";
99
import { Page } from "@/web/components/page";
1010
import type { RegistryItem } from "@/web/components/store/types";
11-
import { useRegistryConnections } from "@/web/hooks/use-binding";
1211
import { useInfiniteScroll } from "@/web/hooks/use-infinite-scroll";
1312
import { useLocalStorage } from "@/web/hooks/use-local-storage";
1413
import { LOCALSTORAGE_KEYS } from "@/web/lib/localstorage-keys";
@@ -806,9 +805,10 @@ function OrgMcpsContent() {
806805

807806
const actions = useConnectionActions();
808807
const connections = useConnections(listState);
809-
// Unfiltered connections for catalog metadata (connectedAppNames, appInstances)
808+
// Connections without search filter for catalog metadata (connectedAppNames, appInstances)
810809
// so the "Connected" badge and modal aren't affected by the search term
811-
const allConnections = useConnections();
810+
const { searchTerm: _ignored, ...listStateWithoutSearch } = listState;
811+
const allConnections = useConnections(listStateWithoutSearch);
812812

813813
const [dialogState, dispatch] = useReducer(dialogReducer, { mode: "idle" });
814814

@@ -864,7 +864,7 @@ function OrgMcpsContent() {
864864
// Optional registry lookup: support multiple registries, let user pick on "All" tab
865865
// Sort so the self/management MCP (Mesh MCP) appears last — external registries like
866866
// Deco Store / MCP Registry should be the default catalog source.
867-
const registryConnections = useRegistryConnections(allConnections).sort(
867+
const registryConnections = useConnections({ binding: "REGISTRY" }).sort(
868868
(a, b) => {
869869
const isSelfA = a.app_name === "@deco/management-mcp";
870870
const isSelfB = b.app_name === "@deco/management-mcp";

apps/mesh/src/web/routes/orgs/store/mcp-server-detail.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
BreadcrumbPage,
1919
BreadcrumbSeparator,
2020
} from "@deco/ui/components/breadcrumb.tsx";
21-
import { useRegistryConnections } from "@/web/hooks/use-binding";
2221
import { usePublisherConnection } from "@/web/hooks/use-publisher-connection";
2322
import {
2423
useConnection,
@@ -282,9 +281,8 @@ function StoreMCPServerDetailContent() {
282281
const [isConnecting, setIsConnecting] = useState(false);
283282
const queryClient = useQueryClient();
284283
const actions = useConnectionActions();
285-
const allConnections = useConnections();
286284
const { data: session } = authClient.useSession();
287-
const registryConnections = useRegistryConnections(allConnections);
285+
const registryConnections = useConnections({ binding: "REGISTRY" });
288286

289287
// Use passed registryId or default to first one
290288
const effectiveRegistryId =
@@ -486,12 +484,21 @@ function StoreMCPServerDetailContent() {
486484
// Combine local and remote tools - prefer local if available
487485
const effectiveTools = hasLocalTools ? data?.tools || [] : remoteTools;
488486

489-
// Get publisher connection from database
487+
// Get publisher connection from database (targeted search by publisher name)
488+
const publisherCandidates = useConnections({
489+
searchTerm: data?.publisher || "",
490+
});
490491
const publisherConnection = usePublisherConnection(
491-
allConnections,
492+
publisherCandidates,
492493
data?.publisher,
493494
);
494495

496+
// Fetch existing connections matching this server for duplicate detection
497+
const serverAppName = selectedItem?.id ?? selectedItem?.title ?? "";
498+
const existingInstallations = useConnections({
499+
searchTerm: serverAppName,
500+
});
501+
495502
// Calculate publisher info
496503
const publisherInfo: PublisherInfo = !data
497504
? { count: 0 }
@@ -702,8 +709,9 @@ function StoreMCPServerDetailContent() {
702709
if (!version) return;
703710

704711
const appName = version.id ?? version.title;
705-
const existing = allConnections.filter(
706-
(c) => c.app_name === appName || c.title === version.title,
712+
const existing = existingInstallations.filter(
713+
(c: ConnectionEntity) =>
714+
c.app_name === appName || c.title === version.title,
707715
);
708716

709717
if (existing.length > 0) {

apps/mesh/src/web/routes/orgs/store/page.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { StoreDiscovery } from "@/web/components/store";
99
import { StoreRegistrySelect } from "@/web/components/store/store-registry-select";
1010
import { StoreRegistryEmptyState } from "@/web/components/store/store-registry-empty-state";
11-
import { useRegistryConnections } from "@/web/hooks/use-binding";
11+
1212
import { useLocalStorage } from "@/web/hooks/use-local-storage";
1313
import { LOCALSTORAGE_KEYS } from "@/web/lib/localstorage-keys";
1414
import {
@@ -71,7 +71,6 @@ function StoreErrorFallback({
7171

7272
export default function StorePage() {
7373
const { org, project } = useProjectContext();
74-
const allConnections = useConnections();
7574
const connectionActions = useConnectionActions();
7675

7776
// Check if we're viewing a child route (server detail)
@@ -80,8 +79,8 @@ export default function StorePage() {
8079
routerState.location.pathname.includes("/store/") &&
8180
routerState.location.pathname.split("/").length > 3;
8281

83-
// Filter to only show registry connections (those with collections)
84-
const allRegistryConnections = useRegistryConnections(allConnections);
82+
// Filter to only show registry connections using server-side binding
83+
const allRegistryConnections = useConnections({ binding: "REGISTRY" });
8584

8685
// The self MCP caches ALL tools (including plugin tools) in its tools column.
8786
// When the private-registry plugin is disabled, the COLLECTION_REGISTRY_APP_*

0 commit comments

Comments
 (0)