Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
880b1d6
feat: add MCP server management functionality to Chat component
Gyurmatag May 12, 2025
5a72f54
Merge branch 'main' into mcp-connections
Gyurmatag May 12, 2025
cf79d62
chore: update agents package version and add session ID to agent conf…
Gyurmatag May 20, 2025
1d97301
feat: integrate DropdownMenu for control panel in Chat component
Gyurmatag May 20, 2025
f7c9b74
feat: enhance DropdownMenu in Chat component with dynamic MCP connect…
Gyurmatag May 20, 2025
8ee3592
feat: extend MCP connection type and update tool count display in Cha…
Gyurmatag May 20, 2025
d238b85
chore: update Tailwind CSS configuration and dependencies
Gyurmatag May 20, 2025
0395159
feat: enhance DropdownMenu styling in Chat component
Gyurmatag May 20, 2025
6f2ce6a
feat: add "Add MCP Server" option to DropdownMenu in Chat component
Gyurmatag May 20, 2025
ecf4b5f
refactor: streamline MCP connections display and interaction in Chat …
Gyurmatag May 20, 2025
a0c5317
feat: implement real-time MCP connection updates in Chat component
Gyurmatag May 20, 2025
96f95e3
refactor: simplify AddMcpServerDialog component by removing localUrl …
Gyurmatag May 20, 2025
835fd3a
feat: integrate react-hook-form for improved form handling in AddMcpS…
Gyurmatag May 20, 2025
a36cda2
refactor: update MCP connection display in Chat component
Gyurmatag May 20, 2025
a4bd5bf
fix: update connection display to include name in Chat component
Gyurmatag May 20, 2025
9d90893
fix: update MCP connection handling in Chat component
Gyurmatag May 20, 2025
a359d92
fix: enhance MCP server dialog validation and submission handling
Gyurmatag May 21, 2025
7940b05
refactor: rename CSS file and update references
Gyurmatag May 21, 2025
510c46f
refactor: remove redundant comments in Chat component
Gyurmatag May 21, 2025
5ead6a4
refactor: reorganize dropdown menu components
Gyurmatag May 21, 2025
5068753
refactor: streamline MCP server management in Chat component
Gyurmatag May 21, 2025
0193a19
refactor: optimize MCP server connection retrieval in server.ts
Gyurmatag May 21, 2025
b26b935
feat: integrate react-hook-form for AddMcpServerDialog
Gyurmatag May 21, 2025
22eb64f
refactor: simplify MCP server connection mapping in Chat component
Gyurmatag May 21, 2025
79d8662
Merge branch 'main' into mcp-connections
Gyurmatag May 21, 2025
7dc233d
feat: update MCP server management and enhance UI components
Gyurmatag May 21, 2025
6aba4c2
refactor: improve MCP server connection mapping for clarity
Gyurmatag May 21, 2025
80dbbef
refactor: update Input import in AddMcpServerDialog for clarity
Gyurmatag May 21, 2025
f6cad97
refactor: update Input import path in AddMcpServerDialog for consistency
Gyurmatag May 21, 2025
b260b26
refactor: correct Input import path in AddMcpServerDialog for consist…
Gyurmatag May 21, 2025
4c97c08
refactor: correct Input import path in AddMcpServerDialog for consist…
Gyurmatag May 21, 2025
f7e8627
refactor: remove commented-out code in Chat component for clarity
Gyurmatag May 21, 2025
1607d0d
refactor: modularize MCP connection handling in Chat component
Gyurmatag May 21, 2025
710bd73
refactor: enhance layout of connection display in Chat component
Gyurmatag May 21, 2025
135ec8f
feat: enhance UI and configuration for MCP server management
Gyurmatag May 21, 2025
0c6e402
chore: update @radix-ui/react-slot dependency and refactor button com…
Gyurmatag May 23, 2025
1b82630
refactor: replace label elements with custom Label component in AddMc…
Gyurmatag May 23, 2025
8662277
refactor: simplify Label component usage in AddMcpServerDialog
Gyurmatag May 23, 2025
2d77e65
refactor: remove destructive variant from Button in Chat component
Gyurmatag May 23, 2025
fa1fcad
Merge origin/main into mcp-connections, accepting mcp-connections cha…
Gyurmatag Aug 27, 2025
ac27c75
Fix formatting issues: unused variable and biome config
Gyurmatag Aug 27, 2025
c51c7d1
Fix npm scripts to use npx for local package resolution
Gyurmatag Aug 27, 2025
1b6f894
Fix React ID uniqueness issues using useId hook
Gyurmatag Aug 27, 2025
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
599 changes: 350 additions & 249 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions package.json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think nanoid is missing from here. If it's working it may be being hoisted into node_modules as a transient dependency - vite caught it after I accidentally installed with pnpm which doesn't hoist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up! I will fix it after @threepointone looked at this PR.

Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
"@biomejs/biome": "^1.9.4",
"@cloudflare/vite-plugin": "1.1.1",
"@cloudflare/vitest-pool-workers": "^0.8.27",
"@cloudflare/workers-types": "^4.20250513.0",
"@tailwindcss/vite": "^4.1.6",
"@cloudflare/workers-types": "^4.20250510.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^22.15.17",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.4.1",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.6",
"tailwindcss": "^4.1.7",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vitest": "3.1.3",
Expand All @@ -42,17 +42,20 @@
"@ai-sdk/ui-utils": "^1.2.11",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@types/marked": "^6.0.0",
"agents": "^0.0.86",
"agents": "^0.0.88",
"ai": "^4.3.15",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.510.0",
"marked": "^15.0.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.4",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.0",
Expand Down
203 changes: 191 additions & 12 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useAgent } from "agents/react";
import { useAgentChat } from "agents/ai-react";
import type { Message } from "@ai-sdk/react";
import type { tools } from "./tools";
import { agentFetch } from "agents/client";
import { nanoid } from "nanoid";

// Component imports
import { Button } from "@/components/button/Button";
Expand All @@ -12,6 +14,15 @@ import { Toggle } from "@/components/toggle/Toggle";
import { Textarea } from "@/components/textarea/Textarea";
import { MemoizedMarkdown } from "@/components/memoized-markdown";
import { ToolInvocationCard } from "@/components/tool-invocation-card/ToolInvocationCard";
import { AddMcpServerDialog } from "@/components/AddMcpServerDialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/dropdown/DropdownMenu";

// Icon imports
import {
Expand All @@ -22,13 +33,23 @@ import {
Trash,
PaperPlaneTilt,
Stop,
Sliders,
} from "@phosphor-icons/react";

// List of tools that require human confirmation
const toolsRequiringConfirmation: (keyof typeof tools)[] = [
"getWeatherInformation",
];

let sessionId = localStorage.getItem("sessionId");
if (!sessionId) {
sessionId = nanoid(8);
localStorage.setItem("sessionId", sessionId);
}

import type { McpConnection } from "./types/mcp";
import { mapMcpServersToConnections } from "./lib/mcp";

export default function Chat() {
const [theme, setTheme] = useState<"dark" | "light">(() => {
// Check localStorage first, default to dark if not found
Expand All @@ -38,6 +59,8 @@ export default function Chat() {
const [showDebug, setShowDebug] = useState(false);
const [textareaHeight, setTextareaHeight] = useState("auto");
const messagesEndRef = useRef<HTMLDivElement>(null);
const [showAddMcpDialog, setShowAddMcpDialog] = useState(false);
const [mcpConnections, setMcpConnections] = useState<McpConnection[]>([]);

const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
Expand Down Expand Up @@ -69,6 +92,10 @@ export default function Chat() {

const agent = useAgent({
agent: "chat",
name: sessionId ?? undefined,
onMcpUpdate: (mcpServersState) => {
setMcpConnections(mapMcpServersToConnections(mcpServersState));
},
});

const {
Expand Down Expand Up @@ -105,9 +132,77 @@ export default function Chat() {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
};

// Helper to open auth popup
function openPopup(authUrl: string) {
window.open(
authUrl,
"popupWindow",
"width=600,height=800,resizable=yes,scrollbars=yes,toolbar=yes,menubar=no,location=no,directories=no,status=yes"
);
}

const handleAddMcpServer = async ({
name,
url,
localUrl,
}: {
name: string;
url: string;
localUrl: string;
}) => {
const res = await agentFetch(
{
host: agent.host,
agent: "chat",
name: sessionId!,
path: "add-mcp",
},
{
method: "POST",
body: JSON.stringify({ url, name, localUrl }),
}
);
// Try to get authUrl from response
try {
const data = (await res.json()) as {
id: string;
url: string;
connectionState: string;
authUrl?: string;
};
if (data?.authUrl) {
openPopup(data.authUrl);
}
} catch (e) {
// ignore if not JSON or no authUrl
}
};

const handleRemoveMcpConnection = async (id: string) => {
await agentFetch(
{
host: agent.host,
agent: "chat",
name: sessionId!,
path: "remove-mcp-connection",
},
{
method: "POST",
body: JSON.stringify({ id }),
}
);
};

return (
<div className="h-[100vh] w-full p-4 flex justify-center items-center bg-fixed overflow-hidden">
<div className="h-[100vh] w-full p-4 flex flex-col items-center bg-fixed overflow-hidden">
<HasOpenAIKey />
<AddMcpServerDialog
open={showAddMcpDialog}
onOpenChange={setShowAddMcpDialog}
onSubmit={({ name, url }) =>
handleAddMcpServer({ name, url, localUrl: "" })
}
/>
<div className="h-[calc(100vh-2rem)] w-full mx-auto max-w-lg flex flex-col shadow-xl rounded-md overflow-hidden relative border border-neutral-300 dark:border-neutral-800">
<div className="px-4 py-3 border-b border-neutral-300 dark:border-neutral-800 flex items-center gap-3 sticky top-0 z-10">
<div className="flex items-center justify-center h-8 w-8">
Expand Down Expand Up @@ -155,7 +250,7 @@ export default function Chat() {
variant="ghost"
size="md"
shape="square"
className="rounded-full h-9 w-9"
className="rounded-full h-9 w-9 cursor-pointer"
onClick={clearHistory}
>
<Trash size={20} />
Expand Down Expand Up @@ -310,7 +405,7 @@ export default function Chat() {
});
setTextareaHeight("auto"); // Reset height after submission
}}
className="p-3 bg-neutral-50 absolute bottom-0 left-0 right-0 z-10 border-t border-neutral-300 dark:border-neutral-800 dark:bg-neutral-900"
className="p-3 bg-neutral-50 absolute bottom-0 left-0 right-0 border-t border-neutral-300 dark:border-neutral-800 dark:bg-neutral-900"
>
<div className="flex items-center gap-2">
<div className="flex-1 relative">
Expand Down Expand Up @@ -344,7 +439,87 @@ export default function Chat() {
rows={2}
style={{ height: textareaHeight }}
/>
<div className="absolute bottom-0 right-0 p-2 w-fit flex flex-row justify-end">
<div className="absolute bottom-0 left-0 p-2 w-fit flex flex-row justify-between">
<div className="flex justify-between w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-1.5 h-fit border border-neutral-200 dark:border-neutral-800"
aria-label="Control Panel"
>
<Sliders size={16} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="top"
className="bg-neutral-50 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 shadow-xl rounded-xl p-1 text-base font-medium text-neutral-900 dark:text-white"
>
<DropdownMenuLabel>MCP Connections</DropdownMenuLabel>
<DropdownMenuSeparator />
{mcpConnections.length === 0 && (
<span className="p-3 text-neutral-500 dark:text-neutral-400 text-sm select-none text-center w-full">
No MCP servers available.
</span>
)}
{mcpConnections.map((conn) => (
<DropdownMenuItem
key={conn.id}
className="flex items-center justify-between w-full"
>
<div className="flex items-center gap-2 w-full">
<span className="flex items-center justify-center w-6 h-6 rounded bg-neutral-100 text-neutral-600 font-semibold text-xs border border-neutral-200 mr-2">
{(conn.name || conn.url).charAt(0).toUpperCase()}
</span>
<div className="flex flex-col">
<span className="text-base text-neutral-900">
{conn.name || conn.url}
</span>
<span
className={`text-xs font-medium lowercase tracking-wide align-middle mt-0.5
${
conn.connectionState === "ready"
? "text-green-600"
: conn.connectionState ===
"authenticating"
? "text-yellow-700"
: "text-red-600"
}
`}
>
{conn.connectionState}
</span>
</div>
</div>
<button
type="button"
className="ml-2 text-red-500 hover:text-red-700 transition-colors cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleRemoveMcpConnection(conn.id);
}}
aria-label="Remove MCP Server"
>
<Trash size={16} />
</button>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
setTimeout(() => setShowAddMcpDialog(true), 0)
}
className="bg-primary/5 text-primary rounded-lg font-semibold px-3 py-2 hover:bg-primary/10 transition-colors cursor-pointer"
>
+ Add MCP Server
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="absolute bottom-0 right-0 p-2 w-fit flex flex-row justify-between">
{isLoading ? (
<button
type="button"
Expand All @@ -355,14 +530,18 @@ export default function Chat() {
<Stop size={16} />
</button>
) : (
<button
type="submit"
className="inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-1.5 h-fit border border-neutral-200 dark:border-neutral-800"
disabled={pendingToolCallConfirmation || !agentInput.trim()}
aria-label="Send message"
>
<PaperPlaneTilt size={16} />
</button>
<div className="flex justify-between w-full">
<button
type="submit"
className="inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 rounded-full p-1.5 h-fit border border-neutral-200 dark:border-neutral-800"
disabled={
pendingToolCallConfirmation || !agentInput.trim()
}
aria-label="Send message"
>
<PaperPlaneTilt size={16} />
</button>
</div>
)}
</div>
</div>
Expand Down
Loading