From da2ce46d4dfe7a9e763579346a5a4c7be369aabf Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Thu, 29 Jan 2026 09:07:22 -0300 Subject: [PATCH 01/29] refactor(monitoring): translate log-row comment to English for code clarity --- apps/mesh/src/web/components/monitoring/log-row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mesh/src/web/components/monitoring/log-row.tsx b/apps/mesh/src/web/components/monitoring/log-row.tsx index caa463aac4..837f966d25 100644 --- a/apps/mesh/src/web/components/monitoring/log-row.tsx +++ b/apps/mesh/src/web/components/monitoring/log-row.tsx @@ -26,7 +26,7 @@ interface LogRowProps { isFirst: boolean; isExpanded: boolean; connection: Connection | undefined; - virtualMcpName: string; // já resolvido pelo pai + virtualMcpName: string; // already resolved by parent onToggle: () => void; lastLogRef?: (node: HTMLDivElement | null) => void; } From 1cfa8f0611d4ce2e358023c7d9f3457059768a45 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Thu, 29 Jan 2026 09:56:06 -0300 Subject: [PATCH 02/29] fix(connections): always call ON_MCP_CONFIGURATION when updating connections Previously, ON_MCP_CONFIGURATION was only called when: 1. configuration_state or configuration_scopes were explicitly passed in the update 2. AND finalState was truthy 3. AND finalScopes.length > 0 This caused a bug where MCPs with onChange handlers were never notified because: - The UI doesn't always send configuration_state/scopes on save - configuration_scopes were never automatically fetched from the MCP - So finalScopes.length was always 0 for new connections Changes: - Fetch scopes from MCP_CONFIGURATION if not already saved - Save fetched scopes to the connection for future updates - Initialize empty state ({}) if scopes exist but state is null - Always call ON_MCP_CONFIGURATION when state and scopes are available - Use single proxy connection for all operations (more efficient) This ensures that MCP onChange handlers are always triggered when a connection is updated, allowing MCPs to properly initialize their state (e.g., caching configs, setting up webhooks, etc.). --- apps/mesh/src/tools/connection/update.ts | 45 +++++++++++++++++------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/mesh/src/tools/connection/update.ts b/apps/mesh/src/tools/connection/update.ts index ac4c66e72f..efd55fa5fd 100644 --- a/apps/mesh/src/tools/connection/update.ts +++ b/apps/mesh/src/tools/connection/update.ts @@ -231,16 +231,34 @@ export const COLLECTION_CONNECTIONS_UPDATE = defineTool({ }; const connection = await ctx.storage.connections.update(id, updatePayload); - // Invoke ON_MCP_CONFIGURATION callback if configuration was updated - // Ignore errors but await for the response before responding - if ( - (data.configuration_state !== undefined || - data.configuration_scopes !== undefined) && - finalState && - finalScopes.length > 0 - ) { - try { - await using proxy = await ctx.createMCPProxy(id); + // Invoke ON_MCP_CONFIGURATION callback to trigger onChange handlers + // If no scopes saved, fetch from MCP_CONFIGURATION first + try { + await using proxy = await ctx.createMCPProxy(id); + + // If no scopes, try to fetch from MCP_CONFIGURATION + if (finalScopes.length === 0) { + const mcpConfig = (await proxy.callTool({ + name: "MCP_CONFIGURATION", + arguments: {}, + })) as { scopes?: string[] }; + + if (mcpConfig?.scopes && mcpConfig.scopes.length > 0) { + finalScopes = mcpConfig.scopes; + // Update connection with fetched scopes + await ctx.storage.connections.update(id, { + configuration_scopes: finalScopes, + }); + } + } + + // Initialize empty state if we have scopes but no state + if (finalScopes.length > 0 && !finalState) { + finalState = {}; + } + + // Call ON_MCP_CONFIGURATION if we have state and scopes + if (finalState && finalScopes.length > 0) { await proxy.callTool({ name: "ON_MCP_CONFIGURATION", arguments: { @@ -248,10 +266,11 @@ export const COLLECTION_CONNECTIONS_UPDATE = defineTool({ scopes: finalScopes, }, }); - await proxy.close().catch(console.error); - } catch (error) { - console.error("Failed to invoke ON_MCP_CONFIGURATION callback", error); } + + await proxy.close().catch(console.error); + } catch (error) { + console.error("Failed to invoke ON_MCP_CONFIGURATION callback", error); } return { From f2d0321ed6fd63b8ce66e4417b279d6238f9fba1 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Mon, 2 Feb 2026 10:43:17 -0300 Subject: [PATCH 03/29] =?UTF-8?q?feat(mesh):=20adiciona=20logs=20detalhado?= =?UTF-8?q?s=20de=20valida=C3=A7=C3=A3o=20no=20endpoint=20decopilot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Loga parseResult.error.format() do Zod quando validação falha - Loga rawPayload que causou erro de validação - Facilita debug de erros 400 'Invalid request body' - Inclui cause no HTTPException para rastreamento --- apps/mesh/src/api/routes/decopilot/routes.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/mesh/src/api/routes/decopilot/routes.ts b/apps/mesh/src/api/routes/decopilot/routes.ts index 3a09aba447..79f92bb8f2 100644 --- a/apps/mesh/src/api/routes/decopilot/routes.ts +++ b/apps/mesh/src/api/routes/decopilot/routes.ts @@ -38,7 +38,18 @@ async function validateRequest( const parseResult = StreamRequestSchema.safeParse(rawPayload); if (!parseResult.success) { - throw new HTTPException(400, { message: "Invalid request body" }); + // Log detalhes do erro de validação para debug + console.error("[Decopilot] ❌ Request validation failed:"); + console.error( + "Validation errors:", + JSON.stringify(parseResult.error.format(), null, 2), + ); + console.error("Raw payload:", JSON.stringify(rawPayload, null, 2)); + + throw new HTTPException(400, { + message: "Invalid request body", + cause: parseResult.error.format(), + }); } return { From 4d9bc88facd6e275e6aec181e6e6cbd6b3aa8458 Mon Sep 17 00:00:00 2001 From: rafavalls Date: Thu, 29 Jan 2026 14:33:13 -0300 Subject: [PATCH 04/29] Updated agent view (#2283) * Enhance virtual MCP functionality and improve error handling - Introduced DEFAULT_SERVER_CAPABILITIES to standardize server capabilities across MCPs. - Updated getServerCapabilities to return the new default capabilities. - Refactored error handling in virtual MCP request processing to ensure proper responses when IDs are missing. - Simplified connection handling in the connection storage by resolving organization IDs more efficiently. - Added new utility functions for managing selections in virtual MCPs, improving the user experience in selection dialogs. - Implemented a new share modal for virtual MCPs, allowing users to easily share agent configurations and integrate with IDEs. - Removed deprecated item selector component to streamline the codebase. * fmt * Refactor ChatPanels component to simplify chat visibility logic - Updated the logic for displaying the chat panel by directly using the `chatOpen` state instead of a derived `shouldShowChat` variable. - Enhanced readability by wrapping the chat-related components in a fragment for better structure. * fix ts --------- Co-authored-by: gimenes --- apps/mesh/src/api/routes/decopilot/routes.ts | 6 +- apps/mesh/src/api/routes/proxy.ts | 24 +- apps/mesh/src/api/routes/virtual-mcp.ts | 8 +- apps/mesh/src/storage/connection.ts | 21 +- apps/mesh/src/storage/ports.ts | 2 +- apps/mesh/src/storage/virtual.ts | 47 +- apps/mesh/src/tools/virtual/schema.ts | 13 +- apps/mesh/src/web/components/chat/input.tsx | 30 +- .../web/components/chat/side-panel-chat.tsx | 4 +- .../src/web/components/chat/tiptap/input.tsx | 14 +- .../collections/collection-search.tsx | 10 +- .../connection-settings-form-ui.tsx | 4 +- .../details/connection/settings-tab/index.tsx | 22 +- .../src/web/components/details/layout.tsx | 2 +- .../dependency-selection-dialog.tsx | 953 +++++++++++ .../components/details/virtual-mcp/index.tsx | 1433 ++++++----------- .../details/virtual-mcp/selection-utils.ts | 114 ++ .../virtual-mcp/virtual-mcp-share-modal.tsx | 370 +++++ .../components/virtual-mcp/item-selector.tsx | 466 ------ .../virtual-mcp/prompt-selector.tsx | 47 - .../virtual-mcp/resource-selector.tsx | 195 --- apps/mesh/src/web/layouts/shell-layout.tsx | 64 +- .../mesh/src/web/routes/orgs/agent-detail.tsx | 6 +- apps/mesh/src/web/routes/orgs/home/page.tsx | 4 +- packages/mesh-sdk/src/index.ts | 1 + packages/mesh-sdk/src/lib/constants.ts | 26 +- .../mesh-sdk/src/lib/server-client-bridge.ts | 12 +- 27 files changed, 2060 insertions(+), 1838 deletions(-) create mode 100644 apps/mesh/src/web/components/details/virtual-mcp/dependency-selection-dialog.tsx create mode 100644 apps/mesh/src/web/components/details/virtual-mcp/selection-utils.ts create mode 100644 apps/mesh/src/web/components/details/virtual-mcp/virtual-mcp-share-modal.tsx delete mode 100644 apps/mesh/src/web/components/virtual-mcp/item-selector.tsx delete mode 100644 apps/mesh/src/web/components/virtual-mcp/prompt-selector.tsx delete mode 100644 apps/mesh/src/web/components/virtual-mcp/resource-selector.tsx diff --git a/apps/mesh/src/api/routes/decopilot/routes.ts b/apps/mesh/src/api/routes/decopilot/routes.ts index 79f92bb8f2..7abca4e604 100644 --- a/apps/mesh/src/api/routes/decopilot/routes.ts +++ b/apps/mesh/src/api/routes/decopilot/routes.ts @@ -11,8 +11,8 @@ import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import type { MeshContext } from "@/core/mesh-context"; +import { createVirtualClientFrom } from "@/mcp-clients/virtual-mcp"; import { generatePrefixedId } from "@/shared/utils/generate-id"; - import { Metadata } from "@/web/components/chat/types"; import { DECOPILOT_BASE_PROMPT, @@ -24,7 +24,6 @@ import { ensureOrganization, toolsFromMCP } from "./helpers"; import { createModelProviderFromProxy } from "./model-provider"; import { StreamRequestSchema } from "./schemas"; import { generateTitleInBackground } from "./title-generator"; -import { createVirtualClientFrom } from "@/mcp-clients/virtual-mcp"; // ============================================================================ // Request Validation @@ -83,9 +82,8 @@ app.post("/:org/decopilot/stream", async (c) => { const threadId = thread_id ?? memoryConfig?.threadId; // Create virtual MCP client and model provider in parallel - // For virtual MCPs (agents), we need to use storage.virtualMcps.findById which handles null const [virtualMcp, modelClient] = await Promise.all([ - ctx.storage.virtualMcps.findById(agent.id ?? null, organization.id), + ctx.storage.virtualMcps.findById(agent.id, organization.id), ctx.createMCPProxy(model.connectionId), ]); diff --git a/apps/mesh/src/api/routes/proxy.ts b/apps/mesh/src/api/routes/proxy.ts index f9d803652f..cf9cef5be7 100644 --- a/apps/mesh/src/api/routes/proxy.ts +++ b/apps/mesh/src/api/routes/proxy.ts @@ -221,6 +221,12 @@ export function toServerClient(client: MCPProxyClient): ServerClient { }; } +const DEFAULT_SERVER_CAPABILITIES = { + tools: {}, + resources: {}, + prompts: {}, +}; + async function createMCPProxyDoNotUseDirectly( connectionIdOrConnection: string | ConnectionEntity, ctx: MeshContext, @@ -558,6 +564,11 @@ async function createMCPProxyDoNotUseDirectly( params: GetPromptRequest["params"], ): Promise => client.getPrompt(params); + // We are currently exposing the underlying client with tools/resources/prompts capabilities + // This way we have an uniform API the frontend can leverage from. + // Frontend connects to mesh. It's garatee that all mcps have the necessary capabilities. The UI works consistently. + const getServerCapabilities = () => DEFAULT_SERVER_CAPABILITIES; + return { callTool: (params: CallToolRequest["params"]) => executeToolCall({ @@ -570,7 +581,7 @@ async function createMCPProxyDoNotUseDirectly( listResourceTemplates, listPrompts, getPrompt, - getServerCapabilities: () => client.getServerCapabilities(), + getServerCapabilities, getInstructions: () => client.getInstructions(), close: () => client.close(), callStreamableTool, @@ -653,16 +664,7 @@ app.all("/:connectionId", async (c) => { await server.connect(transport); // Handle request and cleanup - try { - return await transport.handleRequest(c.req.raw); - } finally { - // Close the transport - try { - await transport.close?.(); - } catch { - // Ignore close errors - transport may already be closed - } - } + return await transport.handleRequest(c.req.raw); } catch (error) { // Check if this is an auth error - if so, return appropriate 401 // Note: This only applies to HTTP connections diff --git a/apps/mesh/src/api/routes/virtual-mcp.ts b/apps/mesh/src/api/routes/virtual-mcp.ts index 839f9266ad..b2c8a0064e 100644 --- a/apps/mesh/src/api/routes/virtual-mcp.ts +++ b/apps/mesh/src/api/routes/virtual-mcp.ts @@ -62,8 +62,12 @@ export async function handleVirtualMcpRequest( .then((org) => org?.id) : null; + if (!virtualMcpId) { + return c.json({ error: "Agent ID or organization ID is required" }, 400); + } + const virtualMcp = await ctx.storage.virtualMcps.findById( - virtualMcpId ?? null, + virtualMcpId, organizationId ?? undefined, ); @@ -85,6 +89,7 @@ export async function handleVirtualMcpRequest( } // Set connection context (Virtual MCPs are now connections) + // Note: virtualMcp.id can be null for Decopilot agent, but connectionId should be set for routing ctx.connectionId = virtualMcp.id ?? undefined; // Set organization context @@ -130,7 +135,6 @@ export async function handleVirtualMcpRequest( // Connect server to transport await server.connect(transport); - // Handle the incoming MCP message return await transport.handleRequest(c.req.raw); } catch (error) { const err = error as Error; diff --git a/apps/mesh/src/storage/connection.ts b/apps/mesh/src/storage/connection.ts index 926ce2ae37..83c4642396 100644 --- a/apps/mesh/src/storage/connection.ts +++ b/apps/mesh/src/storage/connection.ts @@ -102,23 +102,10 @@ export class ConnectionStorage implements ConnectionStoragePort { organizationId?: string, ): Promise { // Handle Decopilot ID - return Decopilot connection entity - if (isDecopilot(id)) { - if (!organizationId) { - // Extract orgId from decopilot_{orgId} pattern - const orgIdMatch = id.match(/^decopilot_(.+)$/); - if (!orgIdMatch) { - throw new Error( - `Invalid Decopilot ID format: ${id}. Expected decopilot_{orgId}`, - ); - } - organizationId = orgIdMatch[1]; - } - if (!organizationId) { - throw new Error( - `Organization ID is required for Decopilot connection: ${id}`, - ); - } - return getWellKnownDecopilotConnection(organizationId); + const decopilotOrgId = isDecopilot(id); + if (decopilotOrgId) { + const resolvedOrgId = organizationId ?? decopilotOrgId; + return getWellKnownDecopilotConnection(resolvedOrgId); } let query = this.db diff --git a/apps/mesh/src/storage/ports.ts b/apps/mesh/src/storage/ports.ts index cfbd98992c..813ef58d17 100644 --- a/apps/mesh/src/storage/ports.ts +++ b/apps/mesh/src/storage/ports.ts @@ -129,7 +129,7 @@ export interface VirtualMCPStoragePort { data: VirtualMCPCreateData, ): Promise; findById( - id: string | null, + id: string, organizationId?: string, ): Promise; list(organizationId: string): Promise; diff --git a/apps/mesh/src/storage/virtual.ts b/apps/mesh/src/storage/virtual.ts index dfe94cb89c..442926d41b 100644 --- a/apps/mesh/src/storage/virtual.ts +++ b/apps/mesh/src/storage/virtual.ts @@ -118,23 +118,13 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort { } async findById( - id: string | null, + id: string, organizationId?: string, ): Promise { // Handle Decopilot ID - return Decopilot agent with all org connections - if (id && isDecopilot(id)) { - // Extract orgId from decopilot_{orgId} pattern or use provided organizationId - const orgIdMatch = id.match(/^decopilot_(.+)$/); - let resolvedOrgId: string; - if (organizationId) { - resolvedOrgId = organizationId; - } else if (orgIdMatch && orgIdMatch[1]) { - resolvedOrgId = orgIdMatch[1]; - } else { - throw new Error( - `Invalid Decopilot ID format: ${id}. Expected decopilot_{orgId} or provide organizationId`, - ); - } + const decopilotOrgId = isDecopilot(id); + if (decopilotOrgId) { + const resolvedOrgId = organizationId ?? decopilotOrgId; // Get all active connections for the organization const connections = await this.db @@ -157,35 +147,6 @@ export class VirtualMCPStorage implements VirtualMCPStoragePort { }; } - // Handle null ID - treat as Decopilot for backward compatibility - if (!id) { - if (!organizationId) { - throw new Error( - "organizationId is required when id is null (Decopilot agent)", - ); - } - - // Get all active connections for the organization - const connections = await this.db - .selectFrom("connections") - .selectAll() - .where("organization_id", "=", organizationId) - .where("status", "!=", "inactive") - .where("status", "!=", "error") - .execute(); - - // Return Decopilot agent with connections populated - return { - ...getWellKnownDecopilotVirtualMCP(organizationId), - connections: connections.map((c) => ({ - connection_id: c.id, - selected_tools: null, // null = all tools - selected_resources: null, // null = all resources - selected_prompts: null, // null = all prompts - })), - }; - } - // Normal database lookup for string IDs return this.findByIdInternal(this.db, id); } diff --git a/apps/mesh/src/tools/virtual/schema.ts b/apps/mesh/src/tools/virtual/schema.ts index ce29735418..fde0294ffb 100644 --- a/apps/mesh/src/tools/virtual/schema.ts +++ b/apps/mesh/src/tools/virtual/schema.ts @@ -40,19 +40,10 @@ export type VirtualMCPConnection = z.infer; */ export const VirtualMCPEntitySchema = z.object({ // Base collection entity fields - id: z - .string() - .nullable() - .describe( - "Unique identifier for the virtual MCP (null for synthetic Decopilot agent)", - ), + id: z.string().describe("Unique identifier for the virtual MCP"), title: z.string().describe("Human-readable name for the virtual MCP"), description: z.string().nullable().describe("Description of the virtual MCP"), - icon: z - .string() - .nullable() - .optional() - .describe("Icon URL for the virtual MCP"), + icon: z.string().nullable().describe("Icon URL for the virtual MCP"), created_at: z.string().describe("When the virtual MCP was created"), updated_at: z.string().describe("When the virtual MCP was last updated"), created_by: z.string().describe("User ID who created the virtual MCP"), diff --git a/apps/mesh/src/web/components/chat/input.tsx b/apps/mesh/src/web/components/chat/input.tsx index bc608d935a..16fecb0b91 100644 --- a/apps/mesh/src/web/components/chat/input.tsx +++ b/apps/mesh/src/web/components/chat/input.tsx @@ -1,5 +1,4 @@ import { IntegrationIcon } from "@/web/components/integration-icon.tsx"; -import { isDecopilot, useProjectContext } from "@decocms/mesh-sdk"; import { getAgentColor } from "@/web/utils/agent-color"; import { Button } from "@deco/ui/components/button.tsx"; import { @@ -8,34 +7,35 @@ import { PopoverTrigger, } from "@deco/ui/components/popover.tsx"; import { cn } from "@deco/ui/lib/utils.ts"; +import { isDecopilot, useProjectContext } from "@decocms/mesh-sdk"; import { useNavigate } from "@tanstack/react-router"; import { AlertCircle, AlertTriangle, ArrowUp, ChevronDown, - Users03, Edit01, Stop, + Users03, XCircle, } from "@untitledui/icons"; import type { FormEvent } from "react"; import { useEffect, useRef, useState, type MouseEvent } from "react"; import { useChat } from "./context"; -import { isTiptapDocEmpty } from "./tiptap/utils"; import { ChatHighlight } from "./index"; +import { ModelSelector } from "./select-model"; import { VirtualMCPPopoverContent, VirtualMCPSelector, type VirtualMCPInfo, } from "./select-virtual-mcp"; -import { ModelSelector } from "./select-model"; +import { FileUploadButton } from "./tiptap/file"; import { - TiptapProvider, TiptapInput, + TiptapProvider, type TiptapInputHandle, } from "./tiptap/input"; -import { FileUploadButton } from "./tiptap/file"; +import { isTiptapDocEmpty } from "./tiptap/utils"; import { UsageStats } from "./usage-stats"; // ============================================================================ @@ -302,16 +302,14 @@ export function ChatInput() { )} > {/* Virtual MCP Badge Header */} - {selectedVirtualMcp && - selectedVirtualMcp.id && - !isDecopilot(selectedVirtualMcp.id) && ( - - )} + {selectedVirtualMcp?.id && !isDecopilot(selectedVirtualMcp.id) && ( + + )} {/* Inner container with the input */}
diff --git a/apps/mesh/src/web/components/chat/side-panel-chat.tsx b/apps/mesh/src/web/components/chat/side-panel-chat.tsx index 4fba1e06b7..3ee91b8eb7 100644 --- a/apps/mesh/src/web/components/chat/side-panel-chat.tsx +++ b/apps/mesh/src/web/components/chat/side-panel-chat.tsx @@ -199,9 +199,7 @@ export function ChatPanel() { return ( }> }> - - - + ); diff --git a/apps/mesh/src/web/components/chat/tiptap/input.tsx b/apps/mesh/src/web/components/chat/tiptap/input.tsx index c6ace74d8f..6a5ffb51a5 100644 --- a/apps/mesh/src/web/components/chat/tiptap/input.tsx +++ b/apps/mesh/src/web/components/chat/tiptap/input.tsx @@ -1,5 +1,6 @@ import { cn } from "@deco/ui/lib/utils.ts"; import Placeholder from "@tiptap/extension-placeholder"; +import type { EditorView } from "@tiptap/pm/view"; import { EditorContent, EditorContext, @@ -7,11 +8,10 @@ import { useEditor, } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; -import type { EditorView } from "@tiptap/pm/view"; import type { Ref } from "react"; -import { useEffect, useImperativeHandle, useRef } from "react"; -import type { VirtualMCPInfo } from "../select-virtual-mcp"; +import { Suspense, useEffect, useImperativeHandle, useRef } from "react"; import type { SelectedModelState } from "../select-model"; +import type { VirtualMCPInfo } from "../select-virtual-mcp"; import type { Metadata } from "../types.ts"; import { FileNode, FileUploader } from "./file"; import { MentionNode } from "./mention"; @@ -184,10 +184,14 @@ export function TiptapInput({ /> {/* Render prompts dropdown menu (includes dialog) */} - + + + {/* Render resources dropdown menu */} - + + + {/* Render file upload handler */} diff --git a/apps/mesh/src/web/components/collections/collection-search.tsx b/apps/mesh/src/web/components/collections/collection-search.tsx index 0815c73530..66b5d6c03f 100644 --- a/apps/mesh/src/web/components/collections/collection-search.tsx +++ b/apps/mesh/src/web/components/collections/collection-search.tsx @@ -10,6 +10,7 @@ interface CollectionSearchProps { className?: string; /** Show a subtle loading spinner when searching in background */ isSearching?: boolean; + disabled?: boolean; } /** @@ -29,12 +30,18 @@ export function CollectionSearch({ onKeyDown, className, isSearching, + disabled, }: CollectionSearchProps) { return (
-
); } /** - * Install on Claude Code Button Component + * Connection Icon Preview Fallback Component - Shows loading state while connection loads */ -function InstallClaudeButton({ url, serverName }: ShareWithNameProps) { - const [copied, setCopied] = useState(false); - - const handleInstall = async () => { - const slugifiedServerName = slugify(serverName); - const connectionConfig = { - type: "http", - url: url, - headers: { - "x-mesh-client": "Claude Code", - }, - }; - const configJson = JSON.stringify(connectionConfig, null, 2); - const command = `claude mcp add-json "${slugifiedServerName}" '${configJson.replace(/'/g, "'\\''")}'`; - - await navigator.clipboard.writeText(command); - setCopied(true); - toast.success("Claude Code command copied to clipboard"); - setTimeout(() => setCopied(false), 2000); - }; - +ConnectionIconPreview.Fallback = function ConnectionIconPreviewFallback() { return ( - +
+
+
); -} +}; /** - * Share Modal - Virtual MCP sharing and IDE integration + * Skill Item Component - Shows a connection with inline badges */ -function VirtualMCPShareModal({ - open, - onOpenChange, - virtualMcp, +function SkillItem({ + connection_id, + selected_tools, + selected_resources, + selected_prompts, + onClick, }: { - open: boolean; - onOpenChange: (open: boolean) => void; - virtualMcp: VirtualMCPEntity; + connection_id: string; + selected_tools: string[] | null; + selected_resources: string[] | null; + selected_prompts: string[] | null; + onClick: () => void; }) { - const [mode, setMode] = useState< - "passthrough" | "smart_tool_selection" | "code_execution" - >("code_execution"); + const connection = useConnection(connection_id); - const handleModeChange = (value: string) => { - if ( - value === "passthrough" || - value === "smart_tool_selection" || - value === "code_execution" - ) { - setMode(value); - } - }; - - // Build URL with mode query parameter - // Virtual MCPs (agents) are accessed via the virtual-mcp endpoint - const virtualMcpUrl = new URL( - `/mcp/virtual-mcp/${virtualMcp.id}`, - window.location.origin, - ); - virtualMcpUrl.searchParams.set("mode", mode); + if (!connection) return null; - // Server name for IDE integrations - const serverName = - virtualMcp.title || `agent-${virtualMcp.id?.slice(0, 8) ?? "default"}`; + // Use getSelectionCount to properly handle null (all selected) vs array + const toolCount = getSelectionCount(selected_tools); + const resourceCount = getSelectionCount(selected_resources); + const promptCount = getSelectionCount(selected_prompts); return ( - - - - Share Agent - -
- {/* Mode Selection */} -
-
-

- How should this agent work? -

-
- - {/* Passthrough Option */} - - - {/* Smart Tool Selection Option */} - - - {/* Code Execution Option */} - - +
+ +

+ {connection.title} +

+ + {toolCount !== 0 && ( +
+ {toolCount === "all" ? "all" : toolCount} +
- - {/* Action Buttons */} -
-
- - - -
+ )} + {resourceCount !== 0 && ( +
+ {resourceCount === "all" ? "all" : resourceCount} +
-
- -
+ )} + {promptCount !== 0 && ( +
+ {promptCount === "all" ? "all" : promptCount} + +
+ )} + + +
); } /** - * Settings Tab - Agent configuration (title, description, icon, system prompt) + * Skill Item Fallback Component - Shows loading state while connection loads */ -function VirtualMCPSettingsTab({ - form, - icon, -}: { - form: ReturnType>; - icon?: string | null; -}) { +SkillItem.Fallback = function SkillItemFallback() { return ( -
-
-
- {/* Header section - Icon, Title, Description */} -
-
- } - /> - ( - -
- - {field.value === "active" ? "Active" : "Inactive"} - - - - field.onChange(checked ? "active" : "inactive") - } - /> - -
- -
- )} - /> -
-
- ( - - - - - - - )} - /> - ( - - - - - - - )} - /> -
-
- - {/* Configuration section */} -
- {/* Selection mode removed - always inclusion mode now */} -
- - {/* System Prompt section */} -
- ( - -
- - Instructions - -

- Define the agent's role, capabilities, and behavior to - guide how it interprets requests, uses available tools, - and responds to users. -

-
- -