From c0d38dc137034a0e47179823622a8bc0661a7a55 Mon Sep 17 00:00:00 2001 From: Dhravya <63950637+Dhravya@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:42:44 +0000 Subject: [PATCH] feat(web): add MCP branding and auth capabilities --- apps/web/app/api/mcp-icon/route.ts | 61 + apps/web/components/brain-connector-icons.tsx | 4 +- .../settings/company-brain-connections.tsx | 47 +- .../settings/mcp-directory-browser.tsx | 68 +- apps/web/lib/mcp-directory.ts | 4 + apps/web/public/mcp-directory.json | 3924 ++++++++++++++--- apps/web/scripts/generate-mcp-directory.py | 64 +- apps/web/scripts/mcp-icon-domains.json | 80 + apps/web/scripts/mcp-oauth-capabilities.json | 289 ++ apps/web/scripts/probe-mcp-oauth.py | 156 + 10 files changed, 4001 insertions(+), 696 deletions(-) create mode 100644 apps/web/app/api/mcp-icon/route.ts create mode 100644 apps/web/scripts/mcp-icon-domains.json create mode 100644 apps/web/scripts/mcp-oauth-capabilities.json create mode 100644 apps/web/scripts/probe-mcp-oauth.py diff --git a/apps/web/app/api/mcp-icon/route.ts b/apps/web/app/api/mcp-icon/route.ts new file mode 100644 index 00000000..b7e364a2 --- /dev/null +++ b/apps/web/app/api/mcp-icon/route.ts @@ -0,0 +1,61 @@ +import { type NextRequest, NextResponse } from "next/server" +import catalog from "../../../public/mcp-directory.json" + +const DOMAIN_RE = + /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i +const MAX_ICON_BYTES = 256 * 1024 +const ALLOWED_DOMAINS = new Set( + catalog.entries.flatMap((entry) => + entry.iconDomain ? [entry.iconDomain] : [], + ), +) + +export async function GET(request: NextRequest) { + const domain = request.nextUrl.searchParams + .get("domain") + ?.trim() + .toLowerCase() + if (!domain || !DOMAIN_RE.test(domain) || !ALLOWED_DOMAINS.has(domain)) { + return new NextResponse(null, { status: 400 }) + } + + const response = await fetch( + `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=128`, + { next: { revalidate: 60 * 60 * 24 * 7 } }, + ) + const contentType = response.headers.get("content-type") ?? "" + if (!response.ok || !contentType.startsWith("image/")) { + return new NextResponse(null, { status: 404 }) + } + const contentLength = Number(response.headers.get("content-length") ?? 0) + if (contentLength > MAX_ICON_BYTES) { + return new NextResponse(null, { status: 413 }) + } + if (!response.body) return new NextResponse(null, { status: 404 }) + const reader = response.body.getReader() + const chunks: Uint8Array[] = [] + let bytes = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + bytes += value.byteLength + if (bytes > MAX_ICON_BYTES) { + await reader.cancel() + return new NextResponse(null, { status: 413 }) + } + chunks.push(value) + } + const body = new Uint8Array(bytes) + let offset = 0 + for (const chunk of chunks) { + body.set(chunk, offset) + offset += chunk.byteLength + } + return new NextResponse(body, { + headers: { + "cache-control": + "public, max-age=86400, s-maxage=604800, stale-while-revalidate=2592000", + "content-type": contentType, + }, + }) +} diff --git a/apps/web/components/brain-connector-icons.tsx b/apps/web/components/brain-connector-icons.tsx index b93ed41a..4bbf6a04 100644 --- a/apps/web/components/brain-connector-icons.tsx +++ b/apps/web/components/brain-connector-icons.tsx @@ -1,5 +1,5 @@ import { cn } from "@lib/utils" -import { Gmail, Granola, Notion } from "@ui/assets/icons" +import { Gmail, GoogleDrive, Granola, Notion } from "@ui/assets/icons" import { dmSans125ClassName } from "@/lib/fonts" export function SlackMark({ className }: { className?: string }) { @@ -99,6 +99,8 @@ export function brainConnectorIcon( className = "size-[18px]", ): React.ReactNode { switch (slug) { + case "google-drive": + return case "gmail": return case "github": diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx index 07a5e817..c5d1fa7f 100644 --- a/apps/web/components/settings/company-brain-connections.tsx +++ b/apps/web/components/settings/company-brain-connections.tsx @@ -523,7 +523,7 @@ export default function CompanyBrainConnections() { setCustomName(entry.name) setCustomServerUrl(entry.url ?? "") setCustomAdvancedOpen(false) - setCustomAuthMethod("oauth") + setCustomAuthMethod(entry.authMethods[0] ?? "oauth") setCustomOpen(true) } @@ -869,22 +869,35 @@ export default function CompanyBrainConnections() { className={customInputClass} /> -
- {(["oauth", "api-key"] as const).map((method) => ( - - ))} +
+ {(["oauth", "api-key"] as const) + .filter( + (method) => + !directoryEntry || + directoryEntry.authMethods.includes(method), + ) + .map((method) => ( + + ))}
{customAuthMethod === "api-key" && ( diff --git a/apps/web/components/settings/mcp-directory-browser.tsx b/apps/web/components/settings/mcp-directory-browser.tsx index 39179627..02ab1cb7 100644 --- a/apps/web/components/settings/mcp-directory-browser.tsx +++ b/apps/web/components/settings/mcp-directory-browser.tsx @@ -1,7 +1,7 @@ "use client" import { cn } from "@lib/utils" -import { ChevronDown, Laptop, Loader2, Search } from "lucide-react" +import { ChevronDown, Loader2, Search } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { dmSans125ClassName } from "@/lib/fonts" import type { McpDirectoryEntry } from "@/lib/mcp-directory" @@ -30,6 +30,14 @@ function isDirectoryEntry(value: unknown): value is McpDirectoryEntry { Array.isArray(entry.categories) && entry.categories.every((category) => typeof category === "string") && typeof entry.popularity === "number" && + (entry.iconDomain === null || typeof entry.iconDomain === "string") && + ["custom", "unsupported"].includes(entry.setup ?? "") && + (entry.oauthCapability === null || + ["dcr", "preregistered"].includes(entry.oauthCapability ?? "")) && + Array.isArray(entry.authMethods) && + entry.authMethods.every((method) => + ["oauth", "api-key"].includes(method), + ) && ["fixed", "tenant", "unavailable", "local"].includes( entry.availability ?? "", ) @@ -72,6 +80,37 @@ function entrySlug(entry: McpDirectoryEntry) { .slice(0, 63) } +function setupLabel(entry: McpDirectoryEntry, builtIn: boolean) { + if (builtIn) return "Built in above" + if (entry.availability !== "fixed" && entry.availability !== "tenant") { + return AVAILABILITY_LABEL[entry.availability] + } + if (entry.oauthCapability === "preregistered") { + return "Requires backend OAuth permissions" + } + if (entry.oauthCapability === "dcr") { + return "OAuth discovery verified; setup pending" + } + if (entry.auth === "no_auth") return "No-auth servers aren't supported yet" + return "Authentication not verified" +} + +function DirectoryIcon({ entry }: { entry: McpDirectoryEntry }) { + const [failed, setFailed] = useState(false) + if (!entry.iconDomain || failed) { + return brainConnectorIcon(entrySlug(entry), entry.name, "size-4") + } + return ( + setFailed(true)} + /> + ) +} + export function McpDirectoryBrowser({ builtInSlugs, onSetUp, @@ -143,9 +182,8 @@ export function McpDirectoryBrowser({

- Browse remote and desktop MCP servers. Remote entries open a setup - form so you can confirm OAuth or API-key authentication before - connecting. + Browse remote and desktop MCP servers. Setup is available only for + connectors with a verified end-to-end authentication flow.

@@ -216,10 +254,8 @@ export function McpDirectoryBrowser({ const builtIn = builtInSlugs.has(entrySlug(entry)) const canSetUp = !builtIn && - entry.auth !== "no_auth" && - !entry.note - ?.toLowerCase() - .includes("register your own oauth client") && + entry.setup !== "unsupported" && + entry.authMethods.length > 0 && (entry.availability === "fixed" || entry.availability === "tenant") const subtitle = @@ -235,11 +271,7 @@ export function McpDirectoryBrowser({ >
- {entry.type === "local" ? ( - - ) : ( - brainConnectorIcon(entrySlug(entry), entry.name, "size-4") - )} +

@@ -252,15 +284,7 @@ export function McpDirectoryBrowser({

- {builtIn - ? "Built in above" - : entry.auth === "no_auth" - ? "No-auth servers aren't supported yet" - : entry.note - ?.toLowerCase() - .includes("register your own oauth client") - ? "Requires your own OAuth client" - : AVAILABILITY_LABEL[entry.availability]} + {setupLabel(entry, builtIn)} {canSetUp ? (