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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ export function createServer(options: ServerOptions = {}): {
return assetExtensions.some(ext => reqPath.toLowerCase().endsWith(ext));
}

// CRITICAL: Static file serving with proper fallback handling
app.use(
express.static(uiBuildPath, {
maxAge: "1y",
Expand Down Expand Up @@ -248,8 +247,6 @@ export function createServer(options: ServerOptions = {}): {
})
);

// CRITICAL: Catch-all route for SPA client-side routing
// This MUST come after static middleware and handle ALL non-API routes
app.get("*", (req, res) => {
const reqPath = req.path;

Expand Down
104 changes: 53 additions & 51 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
useParams,
} from "react-router-dom";
import { MCPLayout } from "@mcpconnect/components";
import { Header, Sidebar, ChatInterface, ConnectionView } from "./components";
import {
Header,
Sidebar,
ChatInterface,
ConnectionView,
ToolDetailPage,
} from "./components";
import { useStorage } from "./contexts/StorageContext";
import { InspectorProvider, InspectorUI } from "./contexts/InspectorProvider";

Expand Down Expand Up @@ -85,56 +91,52 @@ function AppContent() {
};

return (
<Routes>
{/* Main app routes with layout - Lower priority */}
<Route
path="/*"
element={
<InspectorProvider>
<MCPLayout
header={<Header />}
sidebar={
<Sidebar connections={connections} resources={resources} />
}
inspector={<InspectorUI />}
>
<Routes>
<Route
path="/connections"
element={<ConnectionView connections={connections} />}
/>

<Route
path="/connections/:connectionId"
element={<ConnectionChatRedirect />}
/>

<Route
path="/connections/:connectionId/chat"
element={<ConnectionChatRedirect />}
/>

<Route
path="/connections/:connectionId/chat/:chatId"
element={<ChatInterface />}
/>

<Route
path="/connections/:connectionId/chat/:chatId/tools/:toolId"
element={<ChatInterface expandedToolCall={true} />}
/>

{/* Catch-all for main app routes */}
<Route
path="*"
element={<Navigate to="/connections" replace />}
/>
</Routes>
</MCPLayout>
</InspectorProvider>
}
/>
</Routes>
<InspectorProvider>
<MCPLayout
header={<Header />}
sidebar={<Sidebar connections={connections} resources={resources} />}
inspector={<InspectorUI />}
>
<Routes>
{/* Connections overview */}
<Route
path="/connections"
element={<ConnectionView connections={connections} />}
/>

{/* Connection redirects */}
<Route
path="/connections/:connectionId"
element={<ConnectionChatRedirect />}
/>

<Route
path="/connections/:connectionId/chat"
element={<ConnectionChatRedirect />}
/>

{/* Chat interface */}
<Route
path="/connections/:connectionId/chat/:chatId"
element={<ChatInterface />}
/>

<Route
path="/connections/:connectionId/chat/:chatId/tools/:toolId"
element={<ChatInterface expandedToolCall={true} />}
/>

{/* Tool detail page - now uses the same layout */}
<Route
path="/connections/:connectionId/tools/:toolId"
element={<ToolDetailPage />}
/>

{/* Catch-all for main app routes */}
<Route path="*" element={<Navigate to="/connections" replace />} />
</Routes>
</MCPLayout>
</InspectorProvider>
);
}

Expand Down
38 changes: 37 additions & 1 deletion apps/ui/src/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ChatMessage as ChatMessageType } from "@mcpconnect/schemas";
import { useParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
import { useStorage } from "../contexts/StorageContext";
import { useInspector } from "../contexts/InspectorProvider";
Expand Down Expand Up @@ -86,6 +86,7 @@ const preserveMessageOrder = (

export const ChatInterface = (_args: ChatInterfaceProps) => {
const { connectionId, chatId } = useParams();
const navigate = useNavigate();
const {
connections,
systemTools,
Expand Down Expand Up @@ -198,6 +199,40 @@ export const ChatInterface = (_args: ChatInterfaceProps) => {
currentConnection
);

// Tool navigation handler
const handleToolNavigate = useCallback(
(toolId: string, args?: Record<string, any>) => {
if (!connectionId) return;

// Create URL with encoded parameters
const searchParams = new URLSearchParams();

if (args && Object.keys(args).length > 0) {
// Encode each argument as a URL parameter
Object.entries(args).forEach(([key, value]) => {
try {
// JSON stringify and encode the value to preserve type information
const encodedValue = encodeURIComponent(JSON.stringify(value));
searchParams.set(key, encodedValue);
} catch (error) {
// Fallback to string if JSON stringify fails
console.warn(`Failed to encode argument ${key}:`, error);
searchParams.set(key, encodeURIComponent(String(value)));
}
});
}

// Navigate with or without query parameters
const baseUrl = `/connections/${connectionId}/tools/${toolId}`;
const finalUrl = searchParams.toString()
? `${baseUrl}?${searchParams.toString()}`
: baseUrl;

navigate(finalUrl);
},
[connectionId, navigate]
);

// Update streaming refs
useEffect(() => {
updateRefs(conversations, connectionId, chatId);
Expand Down Expand Up @@ -617,6 +652,7 @@ export const ChatInterface = (_args: ChatInterfaceProps) => {
msg.id || `msg-${index}`
)}
onToolCallExpand={handleToolCallExpand}
onToolNavigate={handleToolNavigate}
isToolEnabled={(toolName: string) => {
if (
systemTools.some(tool => tool.name === toolName)
Expand Down
49 changes: 44 additions & 5 deletions apps/ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// apps/ui/src/components/Sidebar.tsx
import { Connection, Resource } from "@mcpconnect/schemas";
import { useNavigate, useLocation, useParams } from "react-router-dom";
import { RconnectLogo } from "./RconnectLogo";
Expand Down Expand Up @@ -99,6 +98,13 @@ export const Sidebar = ({ connections }: SidebarProps) => {
navigate(`/connections/${connection.id}/chat/${firstChatId}`);
};

// Tool navigation handler
const handleToolNavigate = (toolId: string) => {
if (currentConnectionId) {
navigate(`/connections/${currentConnectionId}/tools/${toolId}`);
}
};

// Check if this is a first-time user
const isFirstTime = connections.length === 0;

Expand Down Expand Up @@ -132,8 +138,39 @@ export const Sidebar = ({ connections }: SidebarProps) => {
);
}

// Sort tools: enabled first, then disabled
if (!isFirstTime && currentConnectionId) {
filtered = [...filtered].sort((a, b) => {
let aEnabled: boolean;
let bEnabled: boolean;

if (activeTab === "system") {
aEnabled = isSystemToolEnabled(a.id);
bEnabled = isSystemToolEnabled(b.id);
} else {
aEnabled = isToolEnabled(currentConnectionId, a.id);
bEnabled = isToolEnabled(currentConnectionId, b.id);
}

// Enabled tools come first
if (aEnabled && !bEnabled) return -1;
if (!aEnabled && bEnabled) return 1;

// If both have same enabled state, maintain original order (alphabetical by name)
return a.name.localeCompare(b.name);
});
}

return filtered;
}, [toolsToShow, toolSearchQuery]);
}, [
toolsToShow,
toolSearchQuery,
isFirstTime,
currentConnectionId,
activeTab,
isSystemToolEnabled,
isToolEnabled,
]);

// Tool management functions using reactive storage context
const toggleTool = async (toolId: string) => {
Expand Down Expand Up @@ -446,9 +483,9 @@ export const Sidebar = ({ connections }: SidebarProps) => {
</div>
)}

{/* Tools List - Compact */}
{/* Tools List - Updated with new ToolCard */}
<div
className={`space-y-2 ${isFirstTime ? "opacity-60" : ""} min-w-0 overflow-hidden`}
className={`space-y-3 ${isFirstTime ? "opacity-60" : ""} min-w-0 overflow-hidden`}
>
{filteredTools.length === 0 ? (
<div className="text-center py-4 text-xs text-gray-500 dark:text-gray-400">
Expand Down Expand Up @@ -479,7 +516,9 @@ export const Sidebar = ({ connections }: SidebarProps) => {
key={`${tool.id}-${idx}`}
tool={tool}
enabled={enabled}
onClick={() => toggleTool(tool.id)}
onToggle={() => toggleTool(tool.id)}
onNavigate={handleToolNavigate}
connectionId={currentConnectionId}
isDemoMode={isFirstTime}
/>
);
Expand Down
Loading