mukaddamzaid commited on
Commit
0c91a71
·
1 Parent(s): 3254fc3

feat: enhance sandbox management and error handling

Browse files

- Added detailed logging for sandbox creation and URL updates in startSandbox function.
- Improved error handling for refreshing existing sandboxes and added fallback to create new ones if necessary.
- Updated chat route to handle 404 responses gracefully when fetching chat data.
- Refactored chat component to manage loading states and error notifications more effectively.
- Enhanced MCP server manager to provide clearer feedback on server status changes and actions.

app/actions.ts CHANGED
@@ -86,6 +86,8 @@ export async function startSandbox(params: {
86
  }): Promise<{ url: string }> {
87
  const { id, command, args, env } = params;
88
 
 
 
89
  // Validate required fields
90
  if (!id || !command || !args) {
91
  throw new Error('Missing required fields');
@@ -95,7 +97,27 @@ export async function startSandbox(params: {
95
  if (activeSandboxes.has(id)) {
96
  // If we do, get the URL and return it without creating a new sandbox
97
  const existingSandbox = activeSandboxes.get(id);
98
- return { url: existingSandbox.url };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
  // Build the command string
@@ -123,10 +145,12 @@ export async function startSandbox(params: {
123
  }
124
 
125
  // Start the sandbox
126
- console.log(`Starting sandbox for ${id} with command: ${cmd}`);
127
  const sandbox = await startMcpSandbox({ cmd, envs });
128
  const url = await sandbox.getUrl();
129
 
 
 
130
  // Store the sandbox in our map
131
  activeSandboxes.set(id, { sandbox, url });
132
 
 
86
  }): Promise<{ url: string }> {
87
  const { id, command, args, env } = params;
88
 
89
+ console.log(`[startSandbox] Starting sandbox for ID: ${id}`);
90
+
91
  // Validate required fields
92
  if (!id || !command || !args) {
93
  throw new Error('Missing required fields');
 
97
  if (activeSandboxes.has(id)) {
98
  // If we do, get the URL and return it without creating a new sandbox
99
  const existingSandbox = activeSandboxes.get(id);
100
+ console.log(`[startSandbox] Reusing existing sandbox for ${id}, URL: ${existingSandbox.url}`);
101
+
102
+ // Re-fetch the URL to make sure it's current
103
+ try {
104
+ const freshUrl = await existingSandbox.sandbox.getUrl();
105
+ console.log(`[startSandbox] Updated sandbox URL for ${id}: ${freshUrl}`);
106
+
107
+ // Update the URL in the map
108
+ activeSandboxes.set(id, {
109
+ sandbox: existingSandbox.sandbox,
110
+ url: freshUrl
111
+ });
112
+
113
+ return { url: freshUrl };
114
+ } catch (error) {
115
+ console.error(`[startSandbox] Error refreshing sandbox URL for ${id}:`, error);
116
+
117
+ // Fall through to create a new sandbox if we couldn't refresh the URL
118
+ activeSandboxes.delete(id);
119
+ console.log(`[startSandbox] Removed stale sandbox for ${id}, will create a new one`);
120
+ }
121
  }
122
 
123
  // Build the command string
 
145
  }
146
 
147
  // Start the sandbox
148
+ console.log(`[startSandbox] Creating new sandbox for ${id} with command: ${cmd}`);
149
  const sandbox = await startMcpSandbox({ cmd, envs });
150
  const url = await sandbox.getUrl();
151
 
152
+ console.log(`[startSandbox] Sandbox created for ${id}, URL: ${url}`);
153
+
154
  // Store the sandbox in our map
155
  activeSandboxes.set(id, { sandbox, url });
156
 
app/api/chat/route.ts CHANGED
@@ -67,7 +67,7 @@ export async function POST(req: Request) {
67
  // Generate a title based on first user message
68
  const userMessage = messages.find(m => m.role === 'user');
69
  let title = 'New Chat';
70
-
71
  if (userMessage) {
72
  try {
73
  title = await generateTitle([userMessage]);
@@ -75,7 +75,7 @@ export async function POST(req: Request) {
75
  console.error("Error generating title:", error);
76
  }
77
  }
78
-
79
  // Save the chat immediately so it appears in the sidebar
80
  await saveChat({
81
  id,
@@ -133,11 +133,11 @@ export async function POST(req: Request) {
133
  },
134
  },
135
  anthropic: {
136
- thinking: {
137
- type: 'enabled',
138
- budgetTokens: 12000
139
  },
140
- }
141
  },
142
  experimental_transform: smoothStream({
143
  delayInMs: 5, // optional: defaults to 10ms
@@ -161,7 +161,7 @@ export async function POST(req: Request) {
161
 
162
  const dbMessages = convertToDBMessages(allMessages, id);
163
  await saveMessages({ messages: dbMessages });
164
-
165
  // Clean up resources - now this just closes the client connections
166
  // not the actual servers which persist in the MCP context
167
  await cleanup();
 
67
  // Generate a title based on first user message
68
  const userMessage = messages.find(m => m.role === 'user');
69
  let title = 'New Chat';
70
+
71
  if (userMessage) {
72
  try {
73
  title = await generateTitle([userMessage]);
 
75
  console.error("Error generating title:", error);
76
  }
77
  }
78
+
79
  // Save the chat immediately so it appears in the sidebar
80
  await saveChat({
81
  id,
 
133
  },
134
  },
135
  anthropic: {
136
+ thinking: {
137
+ type: 'enabled',
138
+ budgetTokens: 12000
139
  },
140
+ }
141
  },
142
  experimental_transform: smoothStream({
143
  delayInMs: 5, // optional: defaults to 10ms
 
161
 
162
  const dbMessages = convertToDBMessages(allMessages, id);
163
  await saveMessages({ messages: dbMessages });
164
+
165
  // Clean up resources - now this just closes the client connections
166
  // not the actual servers which persist in the MCP context
167
  await cleanup();
app/chat/[id]/page.tsx CHANGED
@@ -25,22 +25,21 @@ export default function ChatPage() {
25
  await queryClient.prefetchQuery({
26
  queryKey: ['chat', chatId, userId] as const,
27
  queryFn: async () => {
28
- try {
29
- const response = await fetch(`/api/chats/${chatId}`, {
30
- headers: {
31
- 'x-user-id': userId
32
- }
33
- });
34
-
35
- if (!response.ok) {
36
- throw new Error('Failed to load chat');
37
  }
38
-
39
- return response.json();
40
- } catch (error) {
41
- console.error('Error prefetching chat:', error);
42
- return null;
 
 
 
43
  }
 
 
44
  },
45
  staleTime: 1000 * 60 * 5, // 5 minutes
46
  });
 
25
  await queryClient.prefetchQuery({
26
  queryKey: ['chat', chatId, userId] as const,
27
  queryFn: async () => {
28
+ const response = await fetch(`/api/chats/${chatId}`, {
29
+ headers: {
30
+ 'x-user-id': userId
 
 
 
 
 
 
31
  }
32
+ });
33
+
34
+ if (!response.ok) {
35
+ // For 404, return empty chat data instead of throwing
36
+ if (response.status === 404) {
37
+ return { id: chatId, messages: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
38
+ }
39
+ throw new Error('Failed to load chat');
40
  }
41
+
42
+ return response.json();
43
  },
44
  staleTime: 1000 * 60 * 5, // 5 minutes
45
  });
app/layout.tsx CHANGED
@@ -4,6 +4,7 @@ import { ChatSidebar } from "@/components/chat-sidebar";
4
  import { SidebarTrigger } from "@/components/ui/sidebar";
5
  import { Menu } from "lucide-react";
6
  import { Providers } from "./providers";
 
7
  import "./globals.css";
8
  import Script from "next/script";
9
 
@@ -57,6 +58,7 @@ export default function RootLayout({
57
  </main>
58
  </div>
59
  </Providers>
 
60
  <Script defer src="https://cloud.umami.is/script.js" data-website-id="1373896a-fb20-4c9d-b718-c723a2471ae5" />
61
  </body>
62
  </html>
 
4
  import { SidebarTrigger } from "@/components/ui/sidebar";
5
  import { Menu } from "lucide-react";
6
  import { Providers } from "./providers";
7
+ import { Analytics } from "@vercel/analytics/react"
8
  import "./globals.css";
9
  import Script from "next/script";
10
 
 
58
  </main>
59
  </div>
60
  </Providers>
61
+ <Analytics />
62
  <Script defer src="https://cloud.umami.is/script.js" data-website-id="1373896a-fb20-4c9d-b718-c723a2471ae5" />
63
  </body>
64
  </html>
components/chat.tsx CHANGED
@@ -51,30 +51,27 @@ export default function Chat() {
51
  }, [chatId]);
52
 
53
  // Use React Query to fetch chat history
54
- const { data: chatData, isLoading: isLoadingChat } = useQuery({
55
  queryKey: ['chat', chatId, userId] as const,
56
  queryFn: async ({ queryKey }) => {
57
  const [_, chatId, userId] = queryKey;
58
  if (!chatId || !userId) return null;
59
 
60
- try {
61
- const response = await fetch(`/api/chats/${chatId}`, {
62
- headers: {
63
- 'x-user-id': userId
64
- }
65
- });
66
-
67
- if (!response.ok) {
68
- throw new Error('Failed to load chat');
69
  }
70
-
71
- const data = await response.json();
72
- return data as ChatData;
73
- } catch (error) {
74
- console.error('Error loading chat history:', error);
75
- toast.error('Failed to load chat history');
76
- throw error;
 
77
  }
 
 
78
  },
79
  enabled: !!chatId && !!userId,
80
  retry: 1,
@@ -82,6 +79,14 @@ export default function Chat() {
82
  refetchOnWindowFocus: false
83
  });
84
 
 
 
 
 
 
 
 
 
85
  // Prepare initial messages from query data
86
  const initialMessages = useMemo(() => {
87
  if (!chatData || !chatData.messages || chatData.messages.length === 0) {
 
51
  }, [chatId]);
52
 
53
  // Use React Query to fetch chat history
54
+ const { data: chatData, isLoading: isLoadingChat, error } = useQuery({
55
  queryKey: ['chat', chatId, userId] as const,
56
  queryFn: async ({ queryKey }) => {
57
  const [_, chatId, userId] = queryKey;
58
  if (!chatId || !userId) return null;
59
 
60
+ const response = await fetch(`/api/chats/${chatId}`, {
61
+ headers: {
62
+ 'x-user-id': userId
 
 
 
 
 
 
63
  }
64
+ });
65
+
66
+ if (!response.ok) {
67
+ // For 404, return empty chat data instead of throwing
68
+ if (response.status === 404) {
69
+ return { id: chatId, messages: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
70
+ }
71
+ throw new Error('Failed to load chat');
72
  }
73
+
74
+ return response.json() as Promise<ChatData>;
75
  },
76
  enabled: !!chatId && !!userId,
77
  retry: 1,
 
79
  refetchOnWindowFocus: false
80
  });
81
 
82
+ // Handle query errors
83
+ useEffect(() => {
84
+ if (error) {
85
+ console.error('Error loading chat history:', error);
86
+ toast.error('Failed to load chat history');
87
+ }
88
+ }, [error]);
89
+
90
  // Prepare initial messages from query data
91
  const initialMessages = useMemo(() => {
92
  if (!chatData || !chatData.messages || chatData.messages.length === 0) {
components/mcp-server-manager.tsx CHANGED
@@ -196,16 +196,18 @@ export const MCPServerManager = ({
196
 
197
  const toggleServer = (id: string) => {
198
  if (selectedServers.includes(id)) {
199
- // Remove from selected servers
200
  onSelectedServersChange(selectedServers.filter(serverId => serverId !== id));
201
  const server = servers.find(s => s.id === id);
 
202
  if (server) {
203
  toast.success(`Disabled MCP server: ${server.name}`);
204
  }
205
  } else {
206
- // Add to selected servers
207
  onSelectedServersChange([...selectedServers, id]);
208
  const server = servers.find(s => s.id === id);
 
209
  if (server) {
210
  toast.success(`Enabled MCP server: ${server.name}`);
211
  }
@@ -214,6 +216,7 @@ export const MCPServerManager = ({
214
 
215
  const clearAllServers = () => {
216
  if (selectedServers.length > 0) {
 
217
  onSelectedServersChange([]);
218
  toast.success("All MCP servers disabled");
219
  resetAndClose();
@@ -400,57 +403,81 @@ export const MCPServerManager = ({
400
  setShowSensitiveHeaderValues({});
401
  };
402
 
403
- // Add functions to control servers
404
  const toggleServerStatus = async (server: MCPServer, e: React.MouseEvent) => {
405
  e.stopPropagation();
406
 
407
  if (!server.status || server.status === 'disconnected' || server.status === 'error') {
408
- // Start the server
409
  try {
 
410
  const success = await startServer(server.id);
 
411
  if (success) {
412
  toast.success(`Started server: ${server.name}`);
413
  } else {
414
  toast.error(`Failed to start server: ${server.name}`);
415
  }
416
  } catch (error) {
 
 
417
  toast.error(`Error starting server: ${error instanceof Error ? error.message : String(error)}`);
418
  }
419
  } else {
420
- // Stop the server
421
  try {
422
- await stopServer(server.id);
423
- toast.success(`Stopped server: ${server.name}`);
 
 
 
 
424
  } catch (error) {
425
  toast.error(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`);
426
  }
427
  }
428
  };
429
 
430
- // Add function to restart a server
431
  const restartServer = async (server: MCPServer, e: React.MouseEvent) => {
432
  e.stopPropagation();
433
 
434
  try {
435
- // First stop it if it's running
436
  if (server.status === 'connected' || server.status === 'connecting') {
437
  await stopServer(server.id);
438
  }
439
 
440
- // Then start it again
441
- updateServerStatus(server.id, 'connecting');
442
- const success = await startServer(server.id);
443
-
444
- if (success) {
445
- toast.success(`Restarted server: ${server.name}`);
446
- } else {
447
- toast.error(`Failed to restart server: ${server.name}`);
448
- }
 
 
449
  } catch (error) {
 
 
450
  toast.error(`Error restarting server: ${error instanceof Error ? error.message : String(error)}`);
451
  }
452
  };
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  return (
455
  <Dialog open={open} onOpenChange={onOpenChange}>
456
  <DialogContent className="sm:max-w-[480px] max-h-[85vh] overflow-hidden flex flex-col">
@@ -573,10 +600,7 @@ export const MCPServerManager = ({
573
 
574
  {/* Server Details */}
575
  <p className="text-xs text-muted-foreground mb-2.5 truncate">
576
- {server.type === 'sse'
577
- ? server.url
578
- : `${server.command} ${server.args?.join(' ')}`
579
- }
580
  </p>
581
 
582
  {/* Action Button */}
 
196
 
197
  const toggleServer = (id: string) => {
198
  if (selectedServers.includes(id)) {
199
+ // Remove from selected servers but DON'T stop the server
200
  onSelectedServersChange(selectedServers.filter(serverId => serverId !== id));
201
  const server = servers.find(s => s.id === id);
202
+
203
  if (server) {
204
  toast.success(`Disabled MCP server: ${server.name}`);
205
  }
206
  } else {
207
+ // Add to selected servers but DON'T auto-start
208
  onSelectedServersChange([...selectedServers, id]);
209
  const server = servers.find(s => s.id === id);
210
+
211
  if (server) {
212
  toast.success(`Enabled MCP server: ${server.name}`);
213
  }
 
216
 
217
  const clearAllServers = () => {
218
  if (selectedServers.length > 0) {
219
+ // Just deselect all servers without stopping them
220
  onSelectedServersChange([]);
221
  toast.success("All MCP servers disabled");
222
  resetAndClose();
 
403
  setShowSensitiveHeaderValues({});
404
  };
405
 
406
+ // Update functions to control servers
407
  const toggleServerStatus = async (server: MCPServer, e: React.MouseEvent) => {
408
  e.stopPropagation();
409
 
410
  if (!server.status || server.status === 'disconnected' || server.status === 'error') {
 
411
  try {
412
+ updateServerStatus(server.id, 'connecting');
413
  const success = await startServer(server.id);
414
+
415
  if (success) {
416
  toast.success(`Started server: ${server.name}`);
417
  } else {
418
  toast.error(`Failed to start server: ${server.name}`);
419
  }
420
  } catch (error) {
421
+ updateServerStatus(server.id, 'error',
422
+ `Error: ${error instanceof Error ? error.message : String(error)}`);
423
  toast.error(`Error starting server: ${error instanceof Error ? error.message : String(error)}`);
424
  }
425
  } else {
 
426
  try {
427
+ const success = await stopServer(server.id);
428
+ if (success) {
429
+ toast.success(`Stopped server: ${server.name}`);
430
+ } else {
431
+ toast.error(`Failed to stop server: ${server.name}`);
432
+ }
433
  } catch (error) {
434
  toast.error(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`);
435
  }
436
  }
437
  };
438
 
439
+ // Update function to restart a server
440
  const restartServer = async (server: MCPServer, e: React.MouseEvent) => {
441
  e.stopPropagation();
442
 
443
  try {
444
+ // First stop it
445
  if (server.status === 'connected' || server.status === 'connecting') {
446
  await stopServer(server.id);
447
  }
448
 
449
+ // Then start it again (with delay to ensure proper cleanup)
450
+ setTimeout(async () => {
451
+ updateServerStatus(server.id, 'connecting');
452
+ const success = await startServer(server.id);
453
+
454
+ if (success) {
455
+ toast.success(`Restarted server: ${server.name}`);
456
+ } else {
457
+ toast.error(`Failed to restart server: ${server.name}`);
458
+ }
459
+ }, 500);
460
  } catch (error) {
461
+ updateServerStatus(server.id, 'error',
462
+ `Error: ${error instanceof Error ? error.message : String(error)}`);
463
  toast.error(`Error restarting server: ${error instanceof Error ? error.message : String(error)}`);
464
  }
465
  };
466
 
467
+ // UI element to display the correct server URL
468
+ const getServerDisplayUrl = (server: MCPServer): string => {
469
+ // For stdio servers with active sandbox, show the sandbox URL
470
+ if (server.type === 'stdio' && server.sandboxUrl &&
471
+ (server.status === 'connected' || server.status === 'connecting')) {
472
+ return server.sandboxUrl;
473
+ }
474
+
475
+ // Otherwise show the configured URL or command
476
+ return server.type === 'sse'
477
+ ? server.url
478
+ : `${server.command} ${server.args?.join(' ')}`;
479
+ };
480
+
481
  return (
482
  <Dialog open={open} onOpenChange={onOpenChange}>
483
  <DialogContent className="sm:max-w-[480px] max-h-[85vh] overflow-hidden flex flex-col">
 
600
 
601
  {/* Server Details */}
602
  <p className="text-xs text-muted-foreground mb-2.5 truncate">
603
+ {getServerDisplayUrl(server)}
 
 
 
604
  </p>
605
 
606
  {/* Action Button */}
components/message.tsx CHANGED
@@ -1,7 +1,6 @@
1
  "use client";
2
 
3
  import type { Message as TMessage } from "ai";
4
- import { AnimatePresence, motion } from "motion/react";
5
  import { memo, useCallback, useEffect, useState } from "react";
6
  import equal from "fast-deep-equal";
7
  import { Markdown } from "./markdown";
@@ -91,35 +90,28 @@ export function ReasoningMessagePart({
91
  </button>
92
  )}
93
 
94
- <AnimatePresence initial={false}>
95
- {isExpanded && (
96
- <motion.div
97
- key="reasoning"
98
- className={cn(
99
- "text-sm text-muted-foreground flex flex-col gap-2",
100
- "pl-3.5 ml-0.5 mt-1",
101
- "border-l border-amber-200/50 dark:border-amber-700/30"
102
- )}
103
- initial={{ height: 0, opacity: 0 }}
104
- animate={{ height: "auto", opacity: 1 }}
105
- exit={{ height: 0, opacity: 0 }}
106
- transition={{ duration: 0.2, ease: "easeInOut" }}
107
- >
108
- <div className="text-xs text-muted-foreground/70 pl-1 font-medium">
109
- The assistant&apos;s thought process:
110
- </div>
111
- {part.details.map((detail, detailIndex) =>
112
- detail.type === "text" ? (
113
- <div key={detailIndex} className="px-2 py-1.5 bg-muted/10 rounded-md border border-border/30">
114
- <Markdown>{detail.text}</Markdown>
115
- </div>
116
- ) : (
117
- "<redacted>"
118
- ),
119
- )}
120
- </motion.div>
121
- )}
122
- </AnimatePresence>
123
  </div>
124
  );
125
  }
@@ -147,93 +139,88 @@ const PurePreviewMessage = ({
147
  const shouldShowCopyButton = message.role === "assistant" && (!isLatestMessage || status !== "streaming");
148
 
149
  return (
150
- <AnimatePresence key={message.id}>
151
- <motion.div
 
 
 
 
 
 
152
  className={cn(
153
- "w-full mx-auto px-4 group/message",
154
- message.role === "assistant" ? "mb-8" : "mb-6"
155
  )}
156
- initial={{ y: 5, opacity: 0 }}
157
- animate={{ y: 0, opacity: 1 }}
158
- key={`message-${message.id}`}
159
- data-role={message.role}
160
  >
161
- <div
162
- className={cn(
163
- "flex gap-4 w-full group-data-[role=user]/message:ml-auto group-data-[role=user]/message:max-w-2xl",
164
- "group-data-[role=user]/message:w-fit",
165
- )}
166
- >
167
- <div className="flex flex-col w-full space-y-3">
168
- {message.parts?.map((part, i) => {
169
- switch (part.type) {
170
- case "text":
171
- return (
172
- <motion.div
173
- initial={{ y: 5, opacity: 0 }}
174
- animate={{ y: 0, opacity: 1 }}
175
- key={`message-${message.id}-part-${i}`}
176
- className="flex flex-row gap-2 items-start w-full"
177
  >
178
- <div
179
- className={cn("flex flex-col gap-3 w-full", {
180
- "bg-secondary text-secondary-foreground px-4 py-3 rounded-2xl":
181
- message.role === "user",
182
- })}
183
- >
184
- <Markdown>{part.text}</Markdown>
185
- </div>
186
- </motion.div>
187
- );
188
- case "tool-invocation":
189
- const { toolName, state, args } = part.toolInvocation;
190
- const result = 'result' in part.toolInvocation ? part.toolInvocation.result : null;
191
-
192
- return (
193
- <ToolInvocation
194
- key={`message-${message.id}-part-${i}`}
195
- toolName={toolName}
196
- state={state}
197
- args={args}
198
- result={result}
199
- isLatestMessage={isLatestMessage}
200
- status={status}
201
- />
202
- );
203
- case "reasoning":
204
- return (
205
- <ReasoningMessagePart
206
- key={`message-${message.id}-${i}`}
207
- // @ts-expect-error part
208
- part={part}
209
- isReasoning={
210
- (message.parts &&
211
- status === "streaming" &&
212
- i === message.parts.length - 1) ??
213
- false
214
- }
215
- />
216
- );
217
- default:
218
- return null;
219
- }
220
- })}
221
- {shouldShowCopyButton && (
222
- <div className="flex justify-start mt-2">
223
- <CopyButton text={getMessageText()} />
224
- </div>
225
- )}
226
- </div>
227
  </div>
228
- </motion.div>
229
- </AnimatePresence>
230
  );
231
  };
232
 
233
  export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => {
234
  if (prevProps.status !== nextProps.status) return false;
235
- if (prevProps.message.annotations !== nextProps.message.annotations)
236
- return false;
 
 
237
  if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
238
  return true;
239
  });
 
1
  "use client";
2
 
3
  import type { Message as TMessage } from "ai";
 
4
  import { memo, useCallback, useEffect, useState } from "react";
5
  import equal from "fast-deep-equal";
6
  import { Markdown } from "./markdown";
 
90
  </button>
91
  )}
92
 
93
+ {isExpanded && (
94
+ <div
95
+ className={cn(
96
+ "text-sm text-muted-foreground flex flex-col gap-2",
97
+ "pl-3.5 ml-0.5 mt-1",
98
+ "border-l border-amber-200/50 dark:border-amber-700/30"
99
+ )}
100
+ >
101
+ <div className="text-xs text-muted-foreground/70 pl-1 font-medium">
102
+ The assistant&apos;s thought process:
103
+ </div>
104
+ {part.details.map((detail, detailIndex) =>
105
+ detail.type === "text" ? (
106
+ <div key={detailIndex} className="px-2 py-1.5 bg-muted/10 rounded-md border border-border/30">
107
+ <Markdown>{detail.text}</Markdown>
108
+ </div>
109
+ ) : (
110
+ "<redacted>"
111
+ ),
112
+ )}
113
+ </div>
114
+ )}
 
 
 
 
 
 
 
115
  </div>
116
  );
117
  }
 
139
  const shouldShowCopyButton = message.role === "assistant" && (!isLatestMessage || status !== "streaming");
140
 
141
  return (
142
+ <div
143
+ className={cn(
144
+ "w-full mx-auto px-4 group/message",
145
+ message.role === "assistant" ? "mb-8" : "mb-6"
146
+ )}
147
+ data-role={message.role}
148
+ >
149
+ <div
150
  className={cn(
151
+ "flex gap-4 w-full group-data-[role=user]/message:ml-auto group-data-[role=user]/message:max-w-2xl",
152
+ "group-data-[role=user]/message:w-fit",
153
  )}
 
 
 
 
154
  >
155
+ <div className="flex flex-col w-full space-y-3">
156
+ {message.parts?.map((part, i) => {
157
+ switch (part.type) {
158
+ case "text":
159
+ return (
160
+ <div
161
+ key={`message-${message.id}-part-${i}`}
162
+ className="flex flex-row gap-2 items-start w-full"
163
+ >
164
+ <div
165
+ className={cn("flex flex-col gap-3 w-full", {
166
+ "bg-secondary text-secondary-foreground px-4 py-3 rounded-2xl":
167
+ message.role === "user",
168
+ })}
 
 
169
  >
170
+ <Markdown>{part.text}</Markdown>
171
+ </div>
172
+ </div>
173
+ );
174
+ case "tool-invocation":
175
+ const { toolName, state, args } = part.toolInvocation;
176
+ const result = 'result' in part.toolInvocation ? part.toolInvocation.result : null;
177
+
178
+ return (
179
+ <ToolInvocation
180
+ key={`message-${message.id}-part-${i}`}
181
+ toolName={toolName}
182
+ state={state}
183
+ args={args}
184
+ result={result}
185
+ isLatestMessage={isLatestMessage}
186
+ status={status}
187
+ />
188
+ );
189
+ case "reasoning":
190
+ return (
191
+ <ReasoningMessagePart
192
+ key={`message-${message.id}-${i}`}
193
+ // @ts-expect-error part
194
+ part={part}
195
+ isReasoning={
196
+ (message.parts &&
197
+ status === "streaming" &&
198
+ i === message.parts.length - 1) ??
199
+ false
200
+ }
201
+ />
202
+ );
203
+ default:
204
+ return null;
205
+ }
206
+ })}
207
+ {shouldShowCopyButton && (
208
+ <div className="flex justify-start mt-2">
209
+ <CopyButton text={getMessageText()} />
210
+ </div>
211
+ )}
 
 
 
 
 
 
 
212
  </div>
213
+ </div>
214
+ </div>
215
  );
216
  };
217
 
218
  export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => {
219
  if (prevProps.status !== nextProps.status) return false;
220
+ if (prevProps.isLoading !== nextProps.isLoading) return false;
221
+ if (prevProps.isLatestMessage !== nextProps.isLatestMessage) return false;
222
+ if (prevProps.message.annotations !== nextProps.message.annotations) return false;
223
+ if (prevProps.message.id !== nextProps.message.id) return false;
224
  if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
225
  return true;
226
  });
components/tool-invocation.tsx CHANGED
@@ -1,7 +1,6 @@
1
  "use client";
2
 
3
  import { useState } from "react";
4
- import { motion, AnimatePresence } from "motion/react";
5
  import {
6
  ChevronDownIcon,
7
  ChevronUpIcon,
@@ -33,17 +32,6 @@ export function ToolInvocation({
33
  }: ToolInvocationProps) {
34
  const [isExpanded, setIsExpanded] = useState(false);
35
 
36
- const variants = {
37
- collapsed: {
38
- height: 0,
39
- opacity: 0,
40
- },
41
- expanded: {
42
- height: "auto",
43
- opacity: 1,
44
- },
45
- };
46
-
47
  const getStatusIcon = () => {
48
  if (state === "call") {
49
  if (isLatestMessage && status !== "ready") {
@@ -115,48 +103,39 @@ export function ToolInvocation({
115
  </div>
116
  </div>
117
 
118
- <AnimatePresence initial={false}>
119
- {isExpanded && (
120
- <motion.div
121
- initial="collapsed"
122
- animate="expanded"
123
- exit="collapsed"
124
- variants={variants}
125
- transition={{ duration: 0.2 }}
126
- className="space-y-2 px-3 pb-3"
127
- >
128
- {!!args && (
129
- <div className="space-y-1.5">
130
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70 pt-1.5">
131
- <Code className="h-3 w-3" />
132
- <span className="font-medium">Arguments</span>
133
- </div>
134
- <pre className={cn(
135
- "text-xs font-mono p-2.5 rounded-md overflow-x-auto",
136
- "border border-border/40 bg-muted/10"
137
- )}>
138
- {formatContent(args)}
139
- </pre>
140
  </div>
141
- )}
142
-
143
- {!!result && (
144
- <div className="space-y-1.5">
145
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
146
- <ArrowRight className="h-3 w-3" />
147
- <span className="font-medium">Result</span>
148
- </div>
149
- <pre className={cn(
150
- "text-xs font-mono p-2.5 rounded-md overflow-x-auto max-h-[300px] overflow-y-auto",
151
- "border border-border/40 bg-muted/10"
152
- )}>
153
- {formatContent(result)}
154
- </pre>
155
  </div>
156
- )}
157
- </motion.div>
158
- )}
159
- </AnimatePresence>
 
 
 
 
 
 
160
  </div>
161
  );
162
  }
 
1
  "use client";
2
 
3
  import { useState } from "react";
 
4
  import {
5
  ChevronDownIcon,
6
  ChevronUpIcon,
 
32
  }: ToolInvocationProps) {
33
  const [isExpanded, setIsExpanded] = useState(false);
34
 
 
 
 
 
 
 
 
 
 
 
 
35
  const getStatusIcon = () => {
36
  if (state === "call") {
37
  if (isLatestMessage && status !== "ready") {
 
103
  </div>
104
  </div>
105
 
106
+ {isExpanded && (
107
+ <div className="space-y-2 px-3 pb-3">
108
+ {!!args && (
109
+ <div className="space-y-1.5">
110
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70 pt-1.5">
111
+ <Code className="h-3 w-3" />
112
+ <span className="font-medium">Arguments</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
114
+ <pre className={cn(
115
+ "text-xs font-mono p-2.5 rounded-md overflow-x-auto",
116
+ "border border-border/40 bg-muted/10"
117
+ )}>
118
+ {formatContent(args)}
119
+ </pre>
120
+ </div>
121
+ )}
122
+
123
+ {!!result && (
124
+ <div className="space-y-1.5">
125
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
126
+ <ArrowRight className="h-3 w-3" />
127
+ <span className="font-medium">Result</span>
128
  </div>
129
+ <pre className={cn(
130
+ "text-xs font-mono p-2.5 rounded-md overflow-x-auto max-h-[300px] overflow-y-auto",
131
+ "border border-border/40 bg-muted/10"
132
+ )}>
133
+ {formatContent(result)}
134
+ </pre>
135
+ </div>
136
+ )}
137
+ </div>
138
+ )}
139
  </div>
140
  );
141
  }
lib/context/mcp-context.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import React, { createContext, useContext, useEffect, useState, useRef } from "react";
4
  import { useLocalStorage } from "@/lib/hooks/use-local-storage";
5
  import { STORAGE_KEYS } from "@/lib/constants";
6
  import { startSandbox, stopSandbox } from "@/app/actions";
@@ -25,6 +25,7 @@ export interface MCPServer {
25
  description?: string;
26
  status?: ServerStatus;
27
  errorMessage?: string;
 
28
  }
29
 
30
  // Type for processed MCP server config for API
@@ -34,11 +35,6 @@ export interface MCPServerApi {
34
  headers?: KeyValuePair[];
35
  }
36
 
37
- interface SandboxInfo {
38
- id: string;
39
- url: string;
40
- }
41
-
42
  interface MCPContextType {
43
  mcpServers: MCPServer[];
44
  setMcpServers: (servers: MCPServer[]) => void;
@@ -46,50 +42,65 @@ interface MCPContextType {
46
  setSelectedMcpServers: (serverIds: string[]) => void;
47
  mcpServersForApi: MCPServerApi[];
48
  startServer: (serverId: string) => Promise<boolean>;
49
- stopServer: (serverId: string) => Promise<void>;
50
  updateServerStatus: (serverId: string, status: ServerStatus, errorMessage?: string) => void;
 
51
  }
52
 
53
  const MCPContext = createContext<MCPContextType | undefined>(undefined);
54
 
55
  // Helper function to wait for server readiness
56
- async function waitForServerReady(url: string, maxAttempts = 15) {
 
57
  for (let i = 0; i < maxAttempts; i++) {
58
  try {
59
- const response = await fetch(url);
 
 
 
 
 
60
  if (response.status === 200) {
61
  console.log(`Server ready at ${url} after ${i + 1} attempts`);
62
  return true;
63
  }
64
  console.log(`Server not ready yet (attempt ${i + 1}), status: ${response.status}`);
65
- } catch {
66
- console.log(`Server connection failed (attempt ${i + 1})`);
67
  }
68
- // Wait between attempts
69
- await new Promise(resolve => setTimeout(resolve, 6000));
 
 
 
70
  }
 
71
  return false;
72
  }
73
 
74
- export function MCPProvider(props: { children: React.ReactNode }) {
75
- const { children } = props;
76
  const [mcpServers, setMcpServers] = useLocalStorage<MCPServer[]>(
77
  STORAGE_KEYS.MCP_SERVERS,
78
  []
79
  );
 
80
  const [selectedMcpServers, setSelectedMcpServers] = useLocalStorage<string[]>(
81
  STORAGE_KEYS.SELECTED_MCP_SERVERS,
82
  []
83
  );
84
- const [mcpServersForApi, setMcpServersForApi] = useState<MCPServerApi[]>([]);
85
 
86
- // Keep a ref to active sandboxes (only their IDs and URLs)
87
- const sandboxesRef = useRef<SandboxInfo[]>([]);
88
 
 
 
 
 
 
89
  // Update server status
90
  const updateServerStatus = (serverId: string, status: ServerStatus, errorMessage?: string) => {
91
- setMcpServers(current =>
92
- current.map(server =>
93
  server.id === serverId
94
  ? { ...server, status, errorMessage: errorMessage || undefined }
95
  : server
@@ -97,48 +108,79 @@ export function MCPProvider(props: { children: React.ReactNode }) {
97
  );
98
  };
99
 
100
- // Start a server (if it's stdio type) using server actions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  const startServer = async (serverId: string): Promise<boolean> => {
102
- const server = mcpServers.find(s => s.id === serverId);
103
  if (!server) return false;
104
 
105
- // If it's already an SSE server, just update the status
106
- if (server.type === 'sse') {
107
- updateServerStatus(serverId, 'connecting');
108
-
109
- try {
 
110
  const isReady = await waitForServerReady(server.url);
111
  updateServerStatus(serverId, isReady ? 'connected' : 'error',
112
  isReady ? undefined : 'Could not connect to server');
 
 
 
 
 
 
113
  return isReady;
114
- } catch (error) {
115
- updateServerStatus(serverId, 'error', `Connection error: ${error instanceof Error ? error.message : String(error)}`);
116
- return false;
117
  }
118
- }
119
-
120
- // For stdio type, start a sandbox via the server action
121
- if (server.type === 'stdio' && server.command && server.args?.length) {
122
- updateServerStatus(serverId, 'connecting');
123
 
124
- try {
125
- // Check if we already have a sandbox for this server
126
- const existingSandbox = sandboxesRef.current.find(s => s.id === serverId);
127
- if (existingSandbox) {
128
  try {
129
- // Test if the existing sandbox is still responsive
130
- const isReady = await waitForServerReady(existingSandbox.url);
131
  if (isReady) {
132
  updateServerStatus(serverId, 'connected');
 
133
  return true;
134
  }
135
- // If not responsive, we'll create a new one below
136
  } catch {
137
- // Sandbox wasn't responsive, continue to create a new one
138
  }
139
  }
140
 
141
- // Call the server action to create a sandbox
142
  const { url } = await startSandbox({
143
  id: serverId,
144
  command: server.command,
@@ -148,111 +190,72 @@ export function MCPProvider(props: { children: React.ReactNode }) {
148
 
149
  // Wait for the server to become ready
150
  const isReady = await waitForServerReady(url);
151
- if (!isReady) {
152
- updateServerStatus(serverId, 'error', 'Server failed to start in time');
 
 
 
 
 
 
 
 
 
 
153
 
154
- // Attempt to stop the sandbox since it's not working correctly
155
  try {
156
  await stopSandbox(serverId);
157
- } catch (stopError) {
158
- console.error('Failed to stop non-responsive sandbox:', stopError);
159
  }
160
 
161
  return false;
162
  }
163
-
164
- // Store the sandbox reference
165
- // Remove any existing sandbox for this server first
166
- sandboxesRef.current = sandboxesRef.current.filter(s => s.id !== serverId);
167
- sandboxesRef.current.push({ id: serverId, url });
168
-
169
- // Update the server URL to point to the sandbox SSE URL
170
- setMcpServers(current =>
171
- current.map(s =>
172
- s.id === serverId
173
- ? { ...s, status: 'connected', errorMessage: undefined, url }
174
- : s
175
- )
176
- );
177
-
178
- return true;
179
- } catch (error) {
180
- console.error('Error starting server:', error);
181
- updateServerStatus(serverId, 'error', `Startup error: ${error instanceof Error ? error.message : String(error)}`);
182
- return false;
183
- }
184
- }
185
-
186
- return false;
187
- };
188
-
189
- // Stop a server using the server action
190
- const stopServer = async (serverId: string): Promise<void> => {
191
- // Find the sandbox for this server
192
- const sandboxIndex = sandboxesRef.current.findIndex(s => s.id === serverId);
193
- if (sandboxIndex >= 0) {
194
- try {
195
- // Call server action to stop the sandbox
196
- await stopSandbox(serverId);
197
- console.log(`Stopped sandbox for server ${serverId}`);
198
- } catch (error) {
199
- console.error(`Error stopping sandbox for server ${serverId}:`, error);
200
  }
201
 
202
- // Remove from our tracking
203
- sandboxesRef.current.splice(sandboxIndex, 1);
 
 
 
 
 
 
 
204
  }
205
-
206
- // Update server status
207
- updateServerStatus(serverId, 'disconnected');
208
  };
209
-
210
- // Auto-start selected servers when they're added to the selection
211
- useEffect(() => {
212
- const startSelectedServers = async () => {
213
- for (const serverId of selectedMcpServers) {
214
- const server = mcpServers.find(s => s.id === serverId);
215
- if (server && (!server.status || server.status === 'disconnected')) {
216
- await startServer(serverId);
217
- }
218
- }
219
- };
220
-
221
- startSelectedServers();
222
 
223
- // Cleanup on unmount
224
- return () => {
225
- // Stop all running sandboxes
226
- sandboxesRef.current.forEach(async ({ id }) => {
227
  try {
228
- await stopSandbox(id);
 
 
 
 
229
  } catch (error) {
230
- console.error('Error stopping sandbox during cleanup:', error);
231
  }
232
- });
233
- sandboxesRef.current = [];
234
- };
235
- }, [selectedMcpServers]);
236
-
237
- // Process MCP servers for API consumption whenever server data changes
238
- useEffect(() => {
239
- if (!selectedMcpServers.length) {
240
- setMcpServersForApi([]);
241
- return;
242
  }
243
-
244
- const processedServers: MCPServerApi[] = selectedMcpServers
245
- .map(id => mcpServers.find(server => server.id === id))
246
- .filter((server): server is MCPServer => Boolean(server))
247
- .map(server => ({
248
- // All servers are exposed as SSE type to the API
249
- type: 'sse',
250
- url: server.url,
251
- headers: server.headers
252
- }));
253
-
254
- setMcpServersForApi(processedServers);
255
- }, [mcpServers, selectedMcpServers]);
256
 
257
  return (
258
  <MCPContext.Provider
@@ -264,7 +267,8 @@ export function MCPProvider(props: { children: React.ReactNode }) {
264
  mcpServersForApi,
265
  startServer,
266
  stopServer,
267
- updateServerStatus
 
268
  }}
269
  >
270
  {children}
 
1
  "use client";
2
 
3
+ import React, { createContext, useContext, useRef } from "react";
4
  import { useLocalStorage } from "@/lib/hooks/use-local-storage";
5
  import { STORAGE_KEYS } from "@/lib/constants";
6
  import { startSandbox, stopSandbox } from "@/app/actions";
 
25
  description?: string;
26
  status?: ServerStatus;
27
  errorMessage?: string;
28
+ sandboxUrl?: string; // Store the sandbox URL directly on the server object
29
  }
30
 
31
  // Type for processed MCP server config for API
 
35
  headers?: KeyValuePair[];
36
  }
37
 
 
 
 
 
 
38
  interface MCPContextType {
39
  mcpServers: MCPServer[];
40
  setMcpServers: (servers: MCPServer[]) => void;
 
42
  setSelectedMcpServers: (serverIds: string[]) => void;
43
  mcpServersForApi: MCPServerApi[];
44
  startServer: (serverId: string) => Promise<boolean>;
45
+ stopServer: (serverId: string) => Promise<boolean>;
46
  updateServerStatus: (serverId: string, status: ServerStatus, errorMessage?: string) => void;
47
+ getActiveServersForApi: () => MCPServerApi[];
48
  }
49
 
50
  const MCPContext = createContext<MCPContextType | undefined>(undefined);
51
 
52
  // Helper function to wait for server readiness
53
+ async function waitForServerReady(url: string, maxAttempts = 20, timeout = 3000) {
54
+ console.log(`Checking server readiness at ${url}, will try ${maxAttempts} times`);
55
  for (let i = 0; i < maxAttempts; i++) {
56
  try {
57
+ const controller = new AbortController();
58
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
59
+
60
+ const response = await fetch(url, { signal: controller.signal });
61
+ clearTimeout(timeoutId);
62
+
63
  if (response.status === 200) {
64
  console.log(`Server ready at ${url} after ${i + 1} attempts`);
65
  return true;
66
  }
67
  console.log(`Server not ready yet (attempt ${i + 1}), status: ${response.status}`);
68
+ } catch (error) {
69
+ console.log(`Server connection failed (attempt ${i + 1}): ${error instanceof Error ? error.message : 'Unknown error'}`);
70
  }
71
+
72
+ // Wait before next attempt with progressive backoff
73
+ const waitTime = Math.min(1000 * (i + 1), 5000); // Start with 1s, increase each time, max 5s
74
+ console.log(`Waiting ${waitTime}ms before next attempt`);
75
+ await new Promise(resolve => setTimeout(resolve, waitTime));
76
  }
77
+ console.log(`Server failed to become ready after ${maxAttempts} attempts`);
78
  return false;
79
  }
80
 
81
+ export function MCPProvider({ children }: { children: React.ReactNode }) {
 
82
  const [mcpServers, setMcpServers] = useLocalStorage<MCPServer[]>(
83
  STORAGE_KEYS.MCP_SERVERS,
84
  []
85
  );
86
+
87
  const [selectedMcpServers, setSelectedMcpServers] = useLocalStorage<string[]>(
88
  STORAGE_KEYS.SELECTED_MCP_SERVERS,
89
  []
90
  );
 
91
 
92
+ // Create a ref to track active servers and avoid unnecessary re-renders
93
+ const activeServersRef = useRef<Record<string, boolean>>({});
94
 
95
+ // Helper to get a server by ID
96
+ const getServerById = (serverId: string): MCPServer | undefined => {
97
+ return mcpServers.find(server => server.id === serverId);
98
+ };
99
+
100
  // Update server status
101
  const updateServerStatus = (serverId: string, status: ServerStatus, errorMessage?: string) => {
102
+ setMcpServers(currentServers =>
103
+ currentServers.map(server =>
104
  server.id === serverId
105
  ? { ...server, status, errorMessage: errorMessage || undefined }
106
  : server
 
108
  );
109
  };
110
 
111
+ // Update server with sandbox URL
112
+ const updateServerSandboxUrl = (serverId: string, sandboxUrl: string) => {
113
+ console.log(`Storing sandbox URL for server ${serverId}: ${sandboxUrl}`);
114
+
115
+ // Update in memory and force save to localStorage
116
+ setMcpServers(currentServers => {
117
+ const updatedServers = currentServers.map(server =>
118
+ server.id === serverId
119
+ ? { ...server, sandboxUrl, status: 'connected' as ServerStatus }
120
+ : server
121
+ );
122
+
123
+ // Log the updated servers to verify the changes are there
124
+ console.log('Updated server with sandbox URL:',
125
+ updatedServers.find(s => s.id === serverId));
126
+
127
+ // Return the updated servers to set in state and localStorage
128
+ return updatedServers;
129
+ });
130
+ };
131
+
132
+ // Get active servers formatted for API usage
133
+ const getActiveServersForApi = (): MCPServerApi[] => {
134
+ return selectedMcpServers
135
+ .map(id => getServerById(id))
136
+ .filter((server): server is MCPServer => !!server && server.status === 'connected')
137
+ .map(server => ({
138
+ type: 'sse',
139
+ url: server.type === 'stdio' && server.sandboxUrl ? server.sandboxUrl : server.url,
140
+ headers: server.headers
141
+ }));
142
+ };
143
+
144
+ // Start a server
145
  const startServer = async (serverId: string): Promise<boolean> => {
146
+ const server = getServerById(serverId);
147
  if (!server) return false;
148
 
149
+ // Mark server as connecting
150
+ updateServerStatus(serverId, 'connecting');
151
+
152
+ try {
153
+ // For SSE servers, just check if the endpoint is available
154
+ if (server.type === 'sse') {
155
  const isReady = await waitForServerReady(server.url);
156
  updateServerStatus(serverId, isReady ? 'connected' : 'error',
157
  isReady ? undefined : 'Could not connect to server');
158
+
159
+ // Update active servers ref
160
+ if (isReady) {
161
+ activeServersRef.current[serverId] = true;
162
+ }
163
+
164
  return isReady;
 
 
 
165
  }
 
 
 
 
 
166
 
167
+ // For stdio servers, start a sandbox
168
+ if (server.type === 'stdio' && server.command && server.args?.length) {
169
+ // Check if we already have a valid sandbox URL
170
+ if (server.sandboxUrl) {
171
  try {
172
+ const isReady = await waitForServerReady(server.sandboxUrl);
 
173
  if (isReady) {
174
  updateServerStatus(serverId, 'connected');
175
+ activeServersRef.current[serverId] = true;
176
  return true;
177
  }
 
178
  } catch {
179
+ // If sandbox check fails, we'll create a new one
180
  }
181
  }
182
 
183
+ // Create a new sandbox
184
  const { url } = await startSandbox({
185
  id: serverId,
186
  command: server.command,
 
190
 
191
  // Wait for the server to become ready
192
  const isReady = await waitForServerReady(url);
193
+
194
+ if (isReady) {
195
+ // Store the sandbox URL and update status - do this first!
196
+ console.log(`Server ${serverId} started successfully, storing sandbox URL: ${url}`);
197
+ updateServerSandboxUrl(serverId, url);
198
+
199
+ // Mark as active
200
+ activeServersRef.current[serverId] = true;
201
+ return true;
202
+ } else {
203
+ // Failed to start
204
+ updateServerStatus(serverId, 'error', 'Server failed to start');
205
 
206
+ // Clean up sandbox
207
  try {
208
  await stopSandbox(serverId);
209
+ } catch (error) {
210
+ console.error(`Failed to stop non-responsive sandbox ${serverId}:`, error);
211
  }
212
 
213
  return false;
214
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
+ // If we get here, something is misconfigured
218
+ updateServerStatus(serverId, 'error', 'Invalid server configuration');
219
+ return false;
220
+ } catch (error) {
221
+ // Handle any unexpected errors
222
+ console.error(`Error starting server ${serverId}:`, error);
223
+ updateServerStatus(serverId, 'error',
224
+ `Error: ${error instanceof Error ? error.message : String(error)}`);
225
+ return false;
226
  }
 
 
 
227
  };
228
+
229
+ // Stop a server
230
+ const stopServer = async (serverId: string): Promise<boolean> => {
231
+ const server = getServerById(serverId);
232
+ if (!server) return false;
 
 
 
 
 
 
 
 
233
 
234
+ try {
235
+ // For stdio servers with sandbox, stop the sandbox
236
+ if (server.type === 'stdio' && server.sandboxUrl) {
 
237
  try {
238
+ await stopSandbox(serverId);
239
+ console.log(`Stopped sandbox for server ${serverId}`);
240
+
241
+ // Mark as not active
242
+ delete activeServersRef.current[serverId];
243
  } catch (error) {
244
+ console.error(`Error stopping sandbox for server ${serverId}:`, error);
245
  }
246
+ }
247
+
248
+ // Update server status
249
+ updateServerStatus(serverId, 'disconnected');
250
+ return true;
251
+ } catch (error) {
252
+ console.error(`Error stopping server ${serverId}:`, error);
253
+ return false;
 
 
254
  }
255
+ };
256
+
257
+ // Calculate mcpServersForApi based on current state
258
+ const mcpServersForApi = getActiveServersForApi();
 
 
 
 
 
 
 
 
 
259
 
260
  return (
261
  <MCPContext.Provider
 
267
  mcpServersForApi,
268
  startServer,
269
  stopServer,
270
+ updateServerStatus,
271
+ getActiveServersForApi
272
  }}
273
  >
274
  {children}
lib/mcp-client.ts CHANGED
@@ -21,24 +21,6 @@ export interface MCPClientManager {
21
  cleanup: () => Promise<void>;
22
  }
23
 
24
- async function waitForServerReady(url: string, maxAttempts = 5) {
25
- for (let i = 0; i < maxAttempts; i++) {
26
- try {
27
- const response = await fetch(url)
28
- if (response.status === 200) {
29
- console.log(`Server ready at ${url} after ${i + 1} attempts`)
30
- return true
31
- }
32
- console.log(`Server not ready yet (attempt ${i + 1}), status: ${response.status}`)
33
- } catch {
34
- console.log(`Server connection failed (attempt ${i + 1})`)
35
- }
36
- // Wait 6 seconds between attempts
37
- await new Promise(resolve => setTimeout(resolve, 6000))
38
- }
39
- return false
40
- }
41
-
42
  /**
43
  * Initialize MCP clients for API calls
44
  * This uses the already running persistent SSE servers
 
21
  cleanup: () => Promise<void>;
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  /**
25
  * Initialize MCP clients for API calls
26
  * This uses the already running persistent SSE servers
lib/mcp-sandbox.ts CHANGED
@@ -1,5 +1,5 @@
1
  // import Sandbox from "@e2b/code-interpreter";
2
- import { Daytona, Sandbox, SandboxTargetRegion } from "@daytonaio/sdk";
3
 
4
  export const startMcpSandbox = async ({
5
  cmd,
@@ -9,82 +9,95 @@ export const startMcpSandbox = async ({
9
  envs?: Record<string, string>
10
  }) => {
11
  console.log("Creating sandbox...");
12
- const daytona = new Daytona();
13
- const sandbox = await daytona.create({
14
- resources: {
15
- cpu: 2,
16
- memory: 4,
17
- disk: 5,
18
- },
19
- public: true,
20
- autoStopInterval: 0, // 24 hours
21
- timeout: 1000 * 300, // 5 minutes
22
- envVars: {
23
- ...envs,
24
- },
25
- });
 
26
 
27
- const host = await sandbox.getPreviewLink(3000);
28
- const url = host.url;
29
- const token = host.token;
30
- console.log("url", url);
31
- console.log("token", token);
32
 
33
- const sessionId = Math.random().toString(36).substring(2, 30);
34
- await sandbox.process.createSession(sessionId);
35
- // python -m mcp_server_time --local-timezone=Asia/Kolkata
36
- const isPythonCommand = cmd.startsWith('python') || cmd.startsWith('python3');
37
- let installResult = null;
 
38
 
39
- if (isPythonCommand) {
40
- const packageName = cmd.split("-m ")[1]?.split(" ")[0] || "";
41
- if (packageName) {
42
- console.log(`Installing Python package: ${packageName}`);
43
- const installUv = await sandbox.process.executeSessionCommand(sessionId, {
44
- // Install python package from the command after -m in the command
45
- command: `pip install ${packageName}`,
46
- runAsync: true,
47
- },
48
- 1000 * 300 // 5 minutes
49
- );
50
- console.log("install result", installUv.output);
51
- if (installUv.exitCode !== undefined) {
52
- console.error("Failed to install package");
 
 
 
53
  }
54
- installResult = installUv;
55
  }
56
- }
57
 
58
- console.log("Starting mcp server...");
59
- // generate a session with random id
60
- const mcpServer = await sandbox.process.executeSessionCommand(sessionId,
61
- {
62
- command: `npx -y supergateway --base-url ${url} --header "x-daytona-preview-token: ${token}" --port 3000 --cors --stdio "${cmd}"`,
63
- runAsync: true,
64
- },
65
- 1000 * 300 // 5 minutes
66
- );
67
- console.log("mcp server result", mcpServer.output);
 
 
68
 
69
- if (mcpServer.exitCode !== undefined) {
70
- console.error("Failed to start mcp server. Exit code:", mcpServer.exitCode);
71
- }
 
72
 
73
- const session = await sandbox.process.getSession(sessionId);
74
- console.log(`Session ${sessionId}:`);
75
- for (const command of session.commands || []) {
76
- console.log(`Command: ${command.command}, Exit Code: ${command.exitCode}`);
77
- }
 
78
 
79
- console.log("MCP server started at:", url + "/sse");
80
- return new McpSandbox(sandbox);
 
 
 
 
81
  }
82
 
83
  class McpSandbox {
84
  public sandbox: Sandbox;
 
85
 
86
- constructor(sandbox: Sandbox) {
87
  this.sandbox = sandbox;
 
88
  }
89
 
90
  async getUrl(): Promise<string> {
@@ -95,8 +108,26 @@ class McpSandbox {
95
  return `${host.url}/sse`;
96
  }
97
 
 
 
 
 
 
 
 
 
 
98
  async stop(): Promise<void> {
99
- await this.sandbox.delete();
 
 
 
 
 
 
 
 
 
100
  }
101
  }
102
 
 
1
  // import Sandbox from "@e2b/code-interpreter";
2
+ import { Daytona, Sandbox } from "@daytonaio/sdk";
3
 
4
  export const startMcpSandbox = async ({
5
  cmd,
 
9
  envs?: Record<string, string>
10
  }) => {
11
  console.log("Creating sandbox...");
12
+
13
+ try {
14
+ const daytona = new Daytona();
15
+ const sandbox = await daytona.create({
16
+ resources: {
17
+ cpu: 2,
18
+ memory: 4,
19
+ disk: 5,
20
+ },
21
+ public: true,
22
+ autoStopInterval: 0,
23
+ envVars: {
24
+ ...envs,
25
+ },
26
+ });
27
 
28
+ const host = await sandbox.getPreviewLink(3000);
29
+ const url = host.url;
30
+ const token = host.token;
31
+ console.log("url", url);
32
+ console.log("token", token);
33
 
34
+ const sessionId = Math.random().toString(36).substring(2, 30);
35
+ await sandbox.process.createSession(sessionId);
36
+
37
+ // Handle Python package installation if command is a Python command
38
+ const isPythonCommand = cmd.startsWith('python') || cmd.startsWith('python3');
39
+ let installResult = null;
40
 
41
+ if (isPythonCommand) {
42
+ const packageName = cmd.split("-m ")[1]?.split(" ")[0] || "";
43
+ if (packageName) {
44
+ console.log(`Installing Python package: ${packageName}`);
45
+ installResult = await sandbox.process.executeSessionCommand(
46
+ sessionId,
47
+ {
48
+ command: `pip install ${packageName}`,
49
+ runAsync: true,
50
+ },
51
+ 1000 * 300 // 5 minutes
52
+ );
53
+
54
+ console.log("install result", installResult.output);
55
+ if (installResult.exitCode) {
56
+ console.error(`Failed to install package ${packageName}. Exit code: ${installResult.exitCode}`);
57
+ }
58
  }
 
59
  }
 
60
 
61
+ console.log("Starting mcp server...");
62
+ // Run the MCP server using supergateway
63
+ const mcpServer = await sandbox.process.executeSessionCommand(
64
+ sessionId,
65
+ {
66
+ command: `npx -y supergateway --base-url ${url} --header "x-daytona-preview-token: ${token}" --port 3000 --cors --stdio "${cmd}"`,
67
+ runAsync: true,
68
+ },
69
+ 0
70
+ );
71
+
72
+ console.log("mcp server result", mcpServer.output);
73
 
74
+ if (mcpServer.exitCode) {
75
+ console.error("Failed to start mcp server. Exit code:", mcpServer.exitCode);
76
+ throw new Error(`MCP server failed to start with exit code ${mcpServer.exitCode}`);
77
+ }
78
 
79
+ // Log detailed session information
80
+ const session = await sandbox.process.getSession(sessionId);
81
+ console.log(`Session ${sessionId}:`);
82
+ for (const command of session.commands || []) {
83
+ console.log(`Command: ${command.command}, Exit Code: ${command.exitCode}`);
84
+ }
85
 
86
+ console.log("MCP server started at:", url + "/sse");
87
+ return new McpSandbox(sandbox, sessionId);
88
+ } catch (error) {
89
+ console.error("Error starting MCP sandbox:", error);
90
+ throw error;
91
+ }
92
  }
93
 
94
  class McpSandbox {
95
  public sandbox: Sandbox;
96
+ private sessionId?: string;
97
 
98
+ constructor(sandbox: Sandbox, sessionId?: string) {
99
  this.sandbox = sandbox;
100
+ this.sessionId = sessionId;
101
  }
102
 
103
  async getUrl(): Promise<string> {
 
108
  return `${host.url}/sse`;
109
  }
110
 
111
+ async getSessionInfo(): Promise<any> {
112
+ if (!this.sandbox || !this.sessionId) {
113
+ throw new Error("Sandbox or session not initialized");
114
+ }
115
+
116
+ const session = await this.sandbox.process.getSession(this.sessionId);
117
+ return session;
118
+ }
119
+
120
  async stop(): Promise<void> {
121
+ if (!this.sandbox) {
122
+ throw new Error("Sandbox not initialized");
123
+ }
124
+
125
+ try {
126
+ await this.sandbox.delete();
127
+ } catch (error) {
128
+ console.error("Error stopping sandbox:", error);
129
+ throw error;
130
+ }
131
  }
132
  }
133
 
package.json CHANGED
@@ -33,6 +33,7 @@
33
  "@radix-ui/react-slot": "^1.2.0",
34
  "@radix-ui/react-tooltip": "^1.2.3",
35
  "@tanstack/react-query": "^5.74.4",
 
36
  "ai": "^4.3.15",
37
  "class-variance-authority": "^0.7.1",
38
  "clsx": "^2.1.1",
 
33
  "@radix-ui/react-slot": "^1.2.0",
34
  "@radix-ui/react-tooltip": "^1.2.3",
35
  "@tanstack/react-query": "^5.74.4",
36
+ "@vercel/analytics": "^1.5.0",
37
  "ai": "^4.3.15",
38
  "class-variance-authority": "^0.7.1",
39
  "clsx": "^2.1.1",
pnpm-lock.yaml CHANGED
@@ -68,6 +68,9 @@ importers:
68
  '@tanstack/react-query':
69
  specifier: ^5.74.4
70
  version: 5.74.4([email protected])
 
 
 
71
  ai:
72
  specifier: ^4.3.15
73
  version: 4.3.15([email protected])([email protected])
@@ -1675,6 +1678,32 @@ packages:
1675
  cpu: [x64]
1676
  os: [win32]
1677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1678
1679
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
1680
  peerDependencies:
@@ -4891,6 +4920,11 @@ snapshots:
4891
  '@unrs/[email protected]':
4892
  optional: true
4893
 
 
 
 
 
 
4894
4895
  dependencies:
4896
  acorn: 8.14.1
 
68
  '@tanstack/react-query':
69
  specifier: ^5.74.4
70
  version: 5.74.4([email protected])
71
+ '@vercel/analytics':
72
+ specifier: ^1.5.0
73
74
  ai:
75
  specifier: ^4.3.15
76
  version: 4.3.15([email protected])([email protected])
 
1678
  cpu: [x64]
1679
  os: [win32]
1680
 
1681
+ '@vercel/[email protected]':
1682
+ resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
1683
+ peerDependencies:
1684
+ '@remix-run/react': ^2
1685
+ '@sveltejs/kit': ^1 || ^2
1686
+ next: '>= 13'
1687
+ react: ^18 || ^19 || ^19.0.0-rc
1688
+ svelte: '>= 4'
1689
+ vue: ^3
1690
+ vue-router: ^4
1691
+ peerDependenciesMeta:
1692
+ '@remix-run/react':
1693
+ optional: true
1694
+ '@sveltejs/kit':
1695
+ optional: true
1696
+ next:
1697
+ optional: true
1698
+ react:
1699
+ optional: true
1700
+ svelte:
1701
+ optional: true
1702
+ vue:
1703
+ optional: true
1704
+ vue-router:
1705
+ optional: true
1706
+
1707
1708
  resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
1709
  peerDependencies:
 
4920
  '@unrs/[email protected]':
4921
  optional: true
4922
 
4923
4924
+ optionalDependencies:
4925
4926
+ react: 19.1.0
4927
+
4928
4929
  dependencies:
4930
  acorn: 8.14.1