From a5d80b85d95d1e52a34a6251ca55bb11f963bd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Kadir=20Ak=C4=B1n?= Date: Tue, 16 Dec 2025 20:44:09 +0300 Subject: [PATCH] feat(components): add McpConfigTabs component for MCP configuration tabs --- public/mcp-dark.svg | 6 + src/app/docs/api/page.tsx | 17 +- src/app/prompts/page.tsx | 6 +- src/components/mcp/mcp-config-tabs.tsx | 228 ++++++++++++++++ src/components/mcp/mcp-server-popup.tsx | 305 +++++++++++----------- src/components/prompts/prompt-filters.tsx | 180 +++++++++---- src/pages/api/mcp.ts | 10 +- 7 files changed, 533 insertions(+), 219 deletions(-) create mode 100644 public/mcp-dark.svg create mode 100644 src/components/mcp/mcp-config-tabs.tsx diff --git a/public/mcp-dark.svg b/public/mcp-dark.svg new file mode 100644 index 00000000..8303144c --- /dev/null +++ b/public/mcp-dark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/app/docs/api/page.tsx b/src/app/docs/api/page.tsx index 98d3c7d8..46275c60 100644 --- a/src/app/docs/api/page.tsx +++ b/src/app/docs/api/page.tsx @@ -9,6 +9,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { McpConfigTabs } from "@/components/mcp/mcp-config-tabs"; export const metadata = { title: "API Documentation - prompts.chat", @@ -61,19 +62,11 @@ export default async function ApiDocsPage() { Using with MCP Clients

- Add prompts.chat to your MCP client configuration: + Add prompts.chat to your MCP client configuration. Choose your client and connection type below:

-
-
{`{
-  "mcpServers": {
-    "prompts-chat": {
-      "url": "${baseUrl}/api/mcp"
-    }
-  }
-}`}
-
-

- Compatible with Claude Desktop, Cursor, Windsurf, and other MCP-enabled tools. + +

+ Remote connects directly to prompts.chat API. Local runs the MCP server locally via npx.

diff --git a/src/app/prompts/page.tsx b/src/app/prompts/page.tsx index 7168b40e..d74eddff 100644 --- a/src/app/prompts/page.tsx +++ b/src/app/prompts/page.tsx @@ -7,6 +7,7 @@ import { InfinitePromptList } from "@/components/prompts/infinite-prompt-list"; import { PromptFilters } from "@/components/prompts/prompt-filters"; import { FilterProvider } from "@/components/prompts/filter-context"; import { HFDataStudioDropdown } from "@/components/prompts/hf-data-studio-dropdown"; +import { McpServerPopup, McpIcon } from "@/components/mcp/mcp-server-popup"; import { db } from "@/lib/db"; import { isAISearchEnabled, semanticSearch } from "@/lib/ai/embeddings"; import { isAIGenerationEnabled } from "@/lib/ai/generation"; @@ -180,7 +181,10 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
{!config.homepage?.useCloneBranding && ( - +
+ + +
)} + ))} +
+ + {/* Mode Toggle - only show if not hidden */} + {!hideModeToggle && ( +
+ + +
+ )} + + {/* Config Display */} +
+
+
{config}
+
+ +
+ + ); +} diff --git a/src/components/mcp/mcp-server-popup.tsx b/src/components/mcp/mcp-server-popup.tsx index 52c4f2ea..2eedea75 100644 --- a/src/components/mcp/mcp-server-popup.tsx +++ b/src/components/mcp/mcp-server-popup.tsx @@ -2,30 +2,36 @@ import { useState, useMemo } from "react"; import { useTranslations } from "next-intl"; -import { Check, Copy, Plus, X } from "lucide-react"; +import { Plus, X, ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { analyticsMcp } from "@/lib/analytics"; +import { McpConfigTabs } from "./mcp-config-tabs"; -// MCP Logo component -function McpIcon({ className }: { className?: string }) { +// MCP Logo component - shows dark version in dark mode +export function McpIcon({ className }: { className?: string }) { return ( - MCP + <> + MCP + MCP + ); } -export { McpIcon }; - interface McpServerPopupProps { /** Pre-filled users (usernames) */ initialUsers?: string[]; @@ -44,7 +50,8 @@ export function McpServerPopup({ baseUrl, }: McpServerPopupProps) { const t = useTranslations("mcp"); - const [copied, setCopied] = useState(false); + const [filtersOpen, setFiltersOpen] = useState(false); + const [mcpMode, setMcpMode] = useState<"remote" | "local">("remote"); const [users, setUsers] = useState(initialUsers); const [categories, setCategories] = useState(initialCategories); const [tags, setTags] = useState(initialTags); @@ -52,9 +59,8 @@ export function McpServerPopup({ const [categoryInput, setCategoryInput] = useState(""); const [tagInput, setTagInput] = useState(""); - // Build the MCP URL - const mcpUrl = useMemo(() => { - const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "https://prompts.chat"); + // Build query params for MCP URL + const queryParams = useMemo(() => { const params = new URLSearchParams(); if (users.length > 0) { @@ -67,31 +73,8 @@ export function McpServerPopup({ params.set("tags", tags.join(",")); } - const queryString = params.toString(); - return `${base}/api/mcp${queryString ? `?${queryString}` : ""}`; - }, [baseUrl, users, categories, tags]); - - // Generate the JSON config - const configJson = useMemo(() => { - return JSON.stringify( - { - mcpServers: { - "prompts-chat": { - url: mcpUrl, - }, - }, - }, - null, - 2 - ); - }, [mcpUrl]); - - const handleCopy = async () => { - await navigator.clipboard.writeText(configJson); - analyticsMcp.copyCommand("config_json"); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + return params.toString(); + }, [users, categories, tags]); const addUser = () => { const value = userInput.trim().replace(/^@/, ""); @@ -129,134 +112,148 @@ export function McpServerPopup({ {t("button")} - -
- {/* Header */} + +
+ {/* Header with Mode Toggle */}
-

+
- {t("title")} -

- +

{t("title")}

+
+
+ + +
{/* Description */} -

+

{t("description")}

- {/* Config JSON */} -
-
{configJson}
-
+ {/* Config Tabs */} + - {/* Filters */} -
-

{t("customizeFilters")}

- - {/* Users */} -
- -
- setUserInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && addUser()} - placeholder={t("userPlaceholder")} - className="h-7 text-xs flex-1" - /> - -
- {users.length > 0 && ( -
- {users.map((user) => ( - - @{user} - - - ))} + {/* Collapsible Filters */} +
+ + + {filtersOpen && ( +
+ {/* Users */} +
+ +
+ setUserInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addUser()} + placeholder={t("userPlaceholder")} + className="h-6 text-[11px] flex-1" + /> + +
+ {users.length > 0 && ( +
+ {users.map((user) => ( + + @{user} + + + ))} +
+ )}
- )} -
- {/* Categories */} -
- -
- setCategoryInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && addCategory()} - placeholder={t("categoryPlaceholder")} - className="h-7 text-xs flex-1" - /> - -
- {categories.length > 0 && ( -
- {categories.map((cat) => ( - - {cat} - - - ))} + {/* Categories */} +
+ +
+ setCategoryInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addCategory()} + placeholder={t("categoryPlaceholder")} + className="h-6 text-[11px] flex-1" + /> + +
+ {categories.length > 0 && ( +
+ {categories.map((cat) => ( + + {cat} + + + ))} +
+ )}
- )} -
- {/* Tags */} -
- -
- setTagInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && addTag()} - placeholder={t("tagPlaceholder")} - className="h-7 text-xs flex-1" - /> - -
- {tags.length > 0 && ( -
- {tags.map((tag) => ( - - {tag} - - - ))} + {/* Tags */} +
+ +
+ setTagInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addTag()} + placeholder={t("tagPlaceholder")} + className="h-6 text-[11px] flex-1" + /> + +
+ {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + {tag} + + + ))} +
+ )}
- )} -
+
+ )}
diff --git a/src/components/prompts/prompt-filters.tsx b/src/components/prompts/prompt-filters.tsx index 1e43b86e..04e0591b 100644 --- a/src/components/prompts/prompt-filters.tsx +++ b/src/components/prompts/prompt-filters.tsx @@ -15,7 +15,7 @@ import { } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; -import { X, Sparkles, Search } from "lucide-react"; +import { X, Sparkles, Search, SlidersHorizontal } from "lucide-react"; import { analyticsSearch } from "@/lib/analytics"; interface PromptFiltersProps { @@ -49,6 +49,7 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable const searchParams = useSearchParams(); const t = useTranslations(); const [tagSearch, setTagSearch] = useState(""); + const [showFilters, setShowFilters] = useState(false); const { setFilterPending } = useFilterContext(); const debounceRef = useRef(null); @@ -76,64 +77,140 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable const hasFilters = currentFilters.q || currentFilters.type || currentFilters.category || currentFilters.tag || currentFilters.sort; + const activeFilterCount = [currentFilters.type, currentFilters.category, currentFilters.tag, currentFilters.sort && currentFilters.sort !== "newest"].filter(Boolean).length; + return ( -
-
- {t("search.filters")} - +
+ {/* Mobile: Compact search with filter toggle */} +
+ {/* Search bar with AI toggle and filter toggle */} +
+
+ + { + const value = e.target.value; + setFilterPending(true); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + if (value) { + analyticsSearch.search(value, currentFilters.ai === "1"); + } + updateFilter("q", value || null); + }, 300); + }} + /> +
+ {aiSearchEnabled && ( +
+ + { + analyticsSearch.aiSearchToggle(checked); + updateFilter("ai", checked ? "1" : null); + }} + /> +
+ )} + +
+ + {/* Collapsible filters */} + {showFilters && ( +
+
+ {t("search.filters")} + +
+ {/* Mobile filters content rendered below */} +
+ )}
- {/* Search */} -
- - { - const value = e.target.value; - // Show loading immediately - setFilterPending(true); - // Clear previous debounce - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - // Debounce the actual navigation - debounceRef.current = setTimeout(() => { - if (value) { - analyticsSearch.search(value, currentFilters.ai === "1"); + {/* Desktop: Full filters */} +
+
+ {t("search.filters")} + +
+ + {/* Search */} +
+ + { + const value = e.target.value; + setFilterPending(true); + if (debounceRef.current) { + clearTimeout(debounceRef.current); } - updateFilter("q", value || null); - }, 300); - }} - /> -
- - {/* AI Search Toggle */} - {aiSearchEnabled && ( -
- - { - analyticsSearch.aiSearchToggle(checked); - updateFilter("ai", checked ? "1" : null); + debounceRef.current = setTimeout(() => { + if (value) { + analyticsSearch.search(value, currentFilters.ai === "1"); + } + updateFilter("q", value || null); + }, 300); }} />
- )} + {/* AI Search Toggle */} + {aiSearchEnabled && ( +
+ + { + analyticsSearch.aiSearchToggle(checked); + updateFilter("ai", checked ? "1" : null); + }} + /> +
+ )} +
+ + {/* Shared filters - shown on desktop always, on mobile when showFilters is true */} +
{/* Type filter */}
@@ -259,6 +336,7 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable
)} +
); } diff --git a/src/pages/api/mcp.ts b/src/pages/api/mcp.ts index 77b03ee2..ec6311fa 100644 --- a/src/pages/api/mcp.ts +++ b/src/pages/api/mcp.ts @@ -351,7 +351,9 @@ function createServer(options: ServerOptions = {}) { } try { - const result = await extra.sendRequest( + // Add timeout to prevent hanging if client doesn't support elicitation + const timeoutMs = 10000; // 10 seconds + const elicitationPromise = extra.sendRequest( { method: "elicitation/create", params: { @@ -366,6 +368,12 @@ function createServer(options: ServerOptions = {}) { }, ElicitResultSchema ); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs) + ); + + const result = await Promise.race([elicitationPromise, timeoutPromise]); if (result.action === "accept" && result.content) { let filledContent = prompt.content;