Spaces:
Running
Running
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 +26 -2
- app/api/chat/route.ts +7 -7
- app/chat/[id]/page.tsx +13 -14
- app/layout.tsx +2 -0
- components/chat.tsx +22 -17
- components/mcp-server-manager.tsx +46 -22
- components/message.tsx +94 -107
- components/tool-invocation.tsx +31 -52
- lib/context/mcp-context.tsx +138 -134
- lib/mcp-client.ts +0 -18
- lib/mcp-sandbox.ts +94 -63
- package.json +1 -0
- pnpm-lock.yaml +34 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(`
|
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 |
-
|
29 |
-
|
30 |
-
|
31 |
-
'x-user-id': userId
|
32 |
-
}
|
33 |
-
});
|
34 |
-
|
35 |
-
if (!response.ok) {
|
36 |
-
throw new Error('Failed to load chat');
|
37 |
}
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
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 |
-
|
61 |
-
|
62 |
-
|
63 |
-
'x-user-id': userId
|
64 |
-
}
|
65 |
-
});
|
66 |
-
|
67 |
-
if (!response.ok) {
|
68 |
-
throw new Error('Failed to load chat');
|
69 |
}
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
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 |
-
//
|
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 |
-
|
|
|
|
|
|
|
|
|
424 |
} catch (error) {
|
425 |
toast.error(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`);
|
426 |
}
|
427 |
}
|
428 |
};
|
429 |
|
430 |
-
//
|
431 |
const restartServer = async (server: MCPServer, e: React.MouseEvent) => {
|
432 |
e.stopPropagation();
|
433 |
|
434 |
try {
|
435 |
-
// First stop it
|
436 |
if (server.status === 'connected' || server.status === 'connecting') {
|
437 |
await stopServer(server.id);
|
438 |
}
|
439 |
|
440 |
-
// Then start it again
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
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
|
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 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
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 |
-
<
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
className={cn(
|
153 |
-
"w-full
|
154 |
-
|
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 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
key={`message-${message.id}-part-${i}`}
|
176 |
-
className="flex flex-row gap-2 items-start w-full"
|
177 |
>
|
178 |
-
<
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
})}
|
221 |
-
{shouldShowCopyButton && (
|
222 |
-
<div className="flex justify-start mt-2">
|
223 |
-
<CopyButton text={getMessageText()} />
|
224 |
-
</div>
|
225 |
-
)}
|
226 |
-
</div>
|
227 |
</div>
|
228 |
-
</
|
229 |
-
</
|
230 |
);
|
231 |
};
|
232 |
|
233 |
export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => {
|
234 |
if (prevProps.status !== nextProps.status) return false;
|
235 |
-
if (prevProps.
|
236 |
-
|
|
|
|
|
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'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 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
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 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
</
|
155 |
</div>
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
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<
|
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 =
|
|
|
57 |
for (let i = 0; i < maxAttempts; i++) {
|
58 |
try {
|
59 |
-
const
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
69 |
-
|
|
|
|
|
|
|
70 |
}
|
|
|
71 |
return false;
|
72 |
}
|
73 |
|
74 |
-
export function MCPProvider(
|
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 |
-
//
|
87 |
-
const
|
88 |
|
|
|
|
|
|
|
|
|
|
|
89 |
// Update server status
|
90 |
const updateServerStatus = (serverId: string, status: ServerStatus, errorMessage?: string) => {
|
91 |
-
setMcpServers(
|
92 |
-
|
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
const startServer = async (serverId: string): Promise<boolean> => {
|
102 |
-
const server =
|
103 |
if (!server) return false;
|
104 |
|
105 |
-
//
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
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 |
-
|
125 |
-
|
126 |
-
|
127 |
-
if (
|
128 |
try {
|
129 |
-
|
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 |
-
//
|
138 |
}
|
139 |
}
|
140 |
|
141 |
-
//
|
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 |
-
|
152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
|
154 |
-
//
|
155 |
try {
|
156 |
await stopSandbox(serverId);
|
157 |
-
} catch (
|
158 |
-
console.error(
|
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 |
-
//
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
}
|
205 |
-
|
206 |
-
// Update server status
|
207 |
-
updateServerStatus(serverId, 'disconnected');
|
208 |
};
|
209 |
-
|
210 |
-
//
|
211 |
-
|
212 |
-
const
|
213 |
-
|
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 |
-
|
224 |
-
|
225 |
-
|
226 |
-
sandboxesRef.current.forEach(async ({ id }) => {
|
227 |
try {
|
228 |
-
await stopSandbox(
|
|
|
|
|
|
|
|
|
229 |
} catch (error) {
|
230 |
-
console.error(
|
231 |
}
|
232 |
-
}
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
setMcpServersForApi([]);
|
241 |
-
return;
|
242 |
}
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
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
|
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 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
|
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
console.
|
|
|
|
|
|
|
53 |
}
|
54 |
-
installResult = installUv;
|
55 |
}
|
56 |
-
}
|
57 |
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
|
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
78 |
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
version: 1.5.0([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))([email protected])
|
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 |
+
'@vercel/[email protected]([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))([email protected])':
|
4924 |
+
optionalDependencies:
|
4925 |
+
next: 15.3.1(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
|
4926 |
+
react: 19.1.0
|
4927 |
+
|
4928 | |
4929 |
dependencies:
|
4930 |
acorn: 8.14.1
|