mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-23 15:33:52 +00:00
<!-- VORFLUX_AGENT_PR_BODY_BEGIN -->
Adds the full 654-entry MCP directory without bundling records into client JavaScript, with explicit capability status and connector branding that degrades safely when no authoritative logo is available.
## Changes
- Lazy-load and validate the searchable, filterable, progressively rendered MCP catalog.
- Render same-origin proxied provider icons for 543 entries, with a reviewed domain allowlist and deterministic fallback marks for 111 unresolved or unbranded entries.
- Record OAuth discovery capability separately from end-to-end support; all directory setup actions remain suppressed until their authentication flow is verified.
- Add a reproducible OAuth metadata probe with HTTPS/private-network protections, stable URL keys, authorization-server scanning, and catalog fingerprint validation.
- Add Google Drive branding for the curated built-in connector.
## Testing
- **Passed:** Deterministic generation and catalog assertions.
```bash
PATH="$HOME/.bun/bin:$PATH" python3 apps/web/scripts/generate-mcp-directory.py --output
cmp apps/web/public/mcp-directory.json
```
Verified 654 entries, 254 DCR discoveries, 27 preregistered OAuth discoveries, 373 unclassified entries, and zero directory setup actions.
- **Passed:** Stale OAuth metadata fingerprint is rejected by the generator.
- **Passed:** Touched-file Biome checks and `git diff --check`.
- **Passed:** Icon proxy returned 200 for an allowlisted domain and 400 for an unknown valid-looking domain.
- **Passed:** Authenticated desktop/mobile browser inspection and conservative capability labels.
- **Passed:** Public preview returned HTTP 200 and rendered the real app. Authentication cookies do not transfer to the public hostname, so the public screenshot shows login.
- **Partial:** Repository-wide TypeScript checks remain blocked by unrelated existing errors outside the touched MCP files.
- **Partial:** 111 entries intentionally retain deterministic fallback marks; endpoint-derived domains may not always be the canonical brand logo.
- **Blocked:** Google rejected the local HTTP OAuth callback, so live Google Drive consent, callback, persistence, tool discovery, disconnect, and reconnect were not completed.
Public preview: https://ar8ruchhbi65.preview.us1.vorflux.com/configure/tools
---
**Attached Images**
*[288.csv]*
*[mcp-directory-final.json]*



<!-- VORFLUX_AGENT_PR_BODY_END -->
---
**Session Details**
- Session: [View Session](https://supermemory.us1.vorflux.com/agent-sessions/1cd0aab9-2a45-4818-aa13-f9bfe032ddba)
- Requested by: Dhravya Shah (dhravya@supermemory.com)
- Address comments on this PR. Add `(aside)` to your comment to have me ignore it.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes how users pick MCP URLs and auth (OAuth vs API key) before hitting existing connect endpoints; no new backend auth logic in this diff, but misconfiguration or trusting bad URLs remains a user-risk surface.
>
> **Overview**
> Adds a **browseable MCP directory** on the Company Brain connectors page: the catalog is **not bundled in JS**—it loads from static **`/mcp-directory.json`** only after the user opens the directory (with validation, caching, and abort handling).
>
> The new **`McpDirectoryBrowser`** supports search, category/availability filters, and progressive “show more” rendering. Supported remote entries route into the existing custom MCP flow via **Set up**, which pre-fills name/URL and opens the connector dialog with context-specific copy.
>
> The custom connector dialog now uses an explicit **OAuth vs API key** toggle; API key fields only appear for API-key mode, and directory-backed connections get **stable slugs** (`-dir-` suffix) so names display cleanly on connected cards. **Middleware** excludes `mcp-directory.json` from the auth matcher so the asset can be fetched publicly.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8b59bae84a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
"use client"
|
|
|
|
import { cn } from "@lib/utils"
|
|
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"
|
|
import { dmSans125ClassName } from "@/lib/fonts"
|
|
|
|
export const sectionLabelClass = cn(
|
|
dmSans125ClassName(),
|
|
"text-[13px] font-semibold tracking-[-0.01em] text-[#A1A1AA]",
|
|
)
|
|
|
|
// Horizontally scrollable card rail with a section heading — shared by the
|
|
// main integrations directory and the Company Brain connections directory.
|
|
// Arrows appear only when the content actually overflows.
|
|
export function SectionRail({
|
|
label,
|
|
children,
|
|
headerSlot,
|
|
labelSlot,
|
|
scrollbar = "hidden",
|
|
}: {
|
|
label: string
|
|
children: ReactNode
|
|
headerSlot?: ReactNode
|
|
labelSlot?: ReactNode
|
|
scrollbar?: "hidden" | "visible"
|
|
}) {
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
|
const [canScrollRight, setCanScrollRight] = useState(false)
|
|
const [hasOverflow, setHasOverflow] = useState(false)
|
|
|
|
const update = useCallback(() => {
|
|
const el = scrollRef.current
|
|
if (!el) return
|
|
setHasOverflow(el.scrollWidth > el.clientWidth + 4)
|
|
setCanScrollLeft(el.scrollLeft > 4)
|
|
setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
update()
|
|
const el = scrollRef.current
|
|
if (!el) return
|
|
el.addEventListener("scroll", update, { passive: true })
|
|
el.addEventListener("scrollend", update)
|
|
const ro = new ResizeObserver(update)
|
|
ro.observe(el)
|
|
return () => {
|
|
el.removeEventListener("scroll", update)
|
|
el.removeEventListener("scrollend", update)
|
|
ro.disconnect()
|
|
}
|
|
}, [update])
|
|
|
|
const scrollBy = (dir: 1 | -1) => {
|
|
scrollRef.current?.scrollBy({ left: 292 * dir, behavior: "smooth" })
|
|
setTimeout(update, 450)
|
|
}
|
|
|
|
const arrowClass = cn(
|
|
"flex size-7 items-center justify-center rounded-full bg-[#0D121A] text-[#FAFAFA] transition-opacity",
|
|
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]",
|
|
"hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-30",
|
|
)
|
|
|
|
return (
|
|
<section className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
<h3 className={sectionLabelClass}>{label}</h3>
|
|
{labelSlot}
|
|
</div>
|
|
<div className="hidden items-center gap-1.5 sm:flex">
|
|
{headerSlot}
|
|
{hasOverflow ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
aria-label="Show previous"
|
|
disabled={!canScrollLeft}
|
|
onClick={() => scrollBy(-1)}
|
|
className={arrowClass}
|
|
>
|
|
<ArrowLeft className="size-3.5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Show more"
|
|
disabled={!canScrollRight}
|
|
onClick={() => scrollBy(1)}
|
|
className={arrowClass}
|
|
>
|
|
<ArrowRight className="size-3.5" />
|
|
</button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div
|
|
ref={scrollRef}
|
|
className={cn(
|
|
"flex flex-col gap-1.5 sm:-mx-1 sm:flex-row sm:gap-3 sm:overflow-x-auto sm:px-1",
|
|
scrollbar === "visible" ? "scrollbar-thin sm:pb-2" : "scrollbar-none",
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
// Standard card width inside a rail: full-width stacked on mobile, 2-up on
|
|
// small screens, 3-up on large.
|
|
export const railItemClass =
|
|
"w-full sm:shrink-0 sm:grow-0 sm:basis-[calc((100%_-_0.75rem)/2)] lg:basis-[calc((100%_-_1.5rem)/3)]"
|