Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion apps/mesh/src/web/layouts/main-panel-tabs/use-main-panel-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,20 @@ export function useMainPanelTabs(ctx: {
// server is up (shared query keys with Preview / Content). Requires
// SandboxEventsProvider (desktop tabs bar lives inside VmEventsBridge).
const vmEvents = useSandboxEvents();
const { previewUrl } = useSandboxLifecycle();
const { vmEntry, previewUrl } = useSandboxLifecycle();
const devServerReady = vmEvents.lifecycle.phase === "running";

// A user-desktop sandbox serves its dev server on a loopback previewUrl
// (`http://<handle>.localhost`), which the cloud proxy cannot reach — so the
// `dev_<id>` route resolves to a connection the pod can't fetch. The browser
// IS co-located with the daemon, so point the dev MCP client straight at the
// previewUrl (CORS on the deco dev server is `*`). For agent-sandbox the
// previewUrl is public, so leave mcpUrl undefined and keep the cloud route.
const devMcpUrl =
vmEntry?.sandboxProviderKind === "user-desktop" && previewUrl
? `${previewUrl.replace(/\/+$/, "")}/api/mcp`
: undefined;

// Auto-detect the dev MCP app's views straight from its sandbox dev server:
// every tool that declares a `ui://` resource is a view, rendered against the
// ephemeral dev connection (`dev_<id>`). No pairing / curated pinnedViews
Expand All @@ -206,6 +217,7 @@ export function useMainPanelTabs(ctx: {
connectionId: devConnId ?? undefined,
orgId: org.id,
orgSlug: org.slug,
mcpUrl: devMcpUrl,
});
const { data: devToolsResult } = useMCPToolsListQuery({
client: devClient as NonNullable<typeof devClient>,
Expand Down
15 changes: 15 additions & 0 deletions apps/mesh/src/web/routes/project-app-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
useMCPClient,
useMCPToolsList,
useMCPToolCall,
parseDevConnectionId,
} from "@decocms/mesh-sdk";
import { useSandboxLifecycle } from "@/web/components/sandbox/hooks/sandbox-lifecycle-context";
import type {
McpUiDisplayMode,
McpUiMessageRequest,
Expand Down Expand Up @@ -111,10 +113,23 @@ export function AppViewContent({
args?: Record<string, unknown>;
}) {
const { org } = useProjectContext();
const lifecycle = useSandboxLifecycle();
// A dev view (`dev_<id>`) against a user-desktop sandbox renders against the
// loopback dev server, which the cloud proxy can't reach. The browser is
// co-located with the daemon, so connect directly to the previewUrl (deco dev
// server CORS is `*`). Gated on a dev connection id, so regular connection UIs
// and agent-sandbox dev views (public previewUrl) keep the cloud route.
const devMcpUrl =
parseDevConnectionId(connectionId) &&
lifecycle.vmEntry?.sandboxProviderKind === "user-desktop" &&
lifecycle.previewUrl
? `${lifecycle.previewUrl.replace(/\/+$/, "")}/api/mcp`
: undefined;
const client = useMCPClient({
connectionId,
orgId: org.id,
orgSlug: org.slug,
mcpUrl: devMcpUrl,
});
const { data: toolsResult } = useMCPToolsList({ client });

Expand Down
24 changes: 18 additions & 6 deletions packages/mesh-sdk/src/hooks/use-mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export interface CreateMcpClientOptions {
token?: string | null;
/** Mesh server URL - optional, defaults to window.location.origin (for external apps, provide your Mesh server URL) */
meshUrl?: string;
/**
* Absolute MCP endpoint URL override. When set, the client connects directly
* to this URL instead of the org-scoped `/api/:org/mcp/:connectionId` route.
* Used to reach a co-located sandbox dev server straight from the browser
* (loopback `http://<handle>.localhost/api/mcp`), bypassing the cloud proxy —
* which cannot reach a desktop sandbox's loopback. Never the self/REST client.
*/
mcpUrl?: string;
}

export type UseMcpClientOptions = CreateMcpClientOptions;
Expand Down Expand Up @@ -69,18 +77,20 @@ export async function createMCPClient({
orgSlug,
token,
meshUrl,
mcpUrl,
}: CreateMcpClientOptions): Promise<Client> {
const queryKey = KEYS.mcpClient(
orgId,
connectionId ?? "self",
token ?? "",
meshUrl ?? "",
mcpUrl ?? meshUrl ?? "",
);

// Self/management connection → REST-backed client. The browser no longer
// speaks MCP to the mesh server for builtin tools; they're called over plain
// REST (`/api/:org/tools/:name`). The MCP SDK stays for outbound connections.
if (!connectionId || connectionId === SELF_MCP_ALIAS_ID) {
// A direct `mcpUrl` override is never the self client (it targets a dev server).
if (!mcpUrl && (!connectionId || connectionId === SELF_MCP_ALIAS_ID)) {
return createRestSelfClient({
orgSlug,
token,
Expand All @@ -89,7 +99,7 @@ export async function createMCPClient({
});
}

const url = buildMcpUrl(connectionId, orgSlug, meshUrl);
const url = mcpUrl ?? buildMcpUrl(connectionId, orgSlug, meshUrl);

const client = new Client(DEFAULT_CLIENT_INFO, {
capabilities: {
Expand Down Expand Up @@ -133,13 +143,13 @@ export async function createMCPClient({
* cache entry — the key cannot drift between them.
*/
export function mcpClientQueryOptions(options: UseMcpClientOptions) {
const { connectionId, token, meshUrl, orgId } = options;
const { connectionId, token, meshUrl, mcpUrl, orgId } = options;
return {
queryKey: KEYS.mcpClient(
orgId,
connectionId ?? "self",
token ?? "",
meshUrl ?? "",
mcpUrl ?? meshUrl ?? "",
),
queryFn: () => createMCPClient(options),
staleTime: Infinity, // Keep client alive while query is active
Expand Down Expand Up @@ -179,14 +189,15 @@ export function useMCPClientOptional({
orgSlug,
token,
meshUrl,
mcpUrl,
}: UseMcpClientOptionalOptions): Client | null {
const queryKey =
connectionId !== undefined
? KEYS.mcpClient(
orgId,
connectionId ?? "self",
token ?? "",
meshUrl ?? "",
mcpUrl ?? meshUrl ?? "",
)
: (["mcp", "client", "skip", orgId] as const);

Expand All @@ -202,6 +213,7 @@ export function useMCPClientOptional({
orgSlug,
token,
meshUrl,
mcpUrl,
});
},
staleTime: Infinity,
Expand Down
Loading