This commit is contained in:
Dhravya Shah 2026-08-18 18:13:55 -07:00 • committed by GitHub
commit 4d5b629730
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 11112 additions and 32 deletions

View file

@ -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,
},
})
}

View file

@ -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 <GoogleDrive className={className} />
case "gmail":
return <Gmail className={className} />
case "github":

View file

@ -4,6 +4,7 @@ import { useOrgMemberRole } from "@/hooks/use-org-member-role"
import { cn } from "@lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { ChevronDown, Loader2, Plus, XIcon } from "lucide-react"
import dynamic from "next/dynamic"
import { useCallback, useEffect, useState } from "react"
import {
Dialog,
@ -20,9 +21,16 @@ import {
import { toast } from "sonner"
import { dmSans125ClassName } from "@/lib/fonts"
import { useHasCompanyBrain } from "@/hooks/use-company-brain"
import type { McpDirectoryEntry } from "@/lib/mcp-directory"
import { brainConnectorIcon, SlackMark } from "../brain-connector-icons"
import { PillButton } from "../integrations/install-steps"
const McpDirectoryBrowser = dynamic(() =>
import("./mcp-directory-browser").then(
(module) => module.McpDirectoryBrowser,
),
)
const BACKEND =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
@ -58,6 +66,19 @@ function slugifyMcpName(value: string) {
.slice(0, 63)
}
function customConnectionName(slug: string) {
return titleCase(slug.replace(/-sm-dir-[a-z0-9]{6}$/, "").replace(/-/g, " "))
}
function stableDirectorySuffix(value: string) {
let hash = 0x811c9dc5
for (const character of value) {
hash ^= character.codePointAt(0) ?? 0
hash = Math.imul(hash, 0x01000193)
}
return (hash >>> 0).toString(36).slice(0, 6).padStart(6, "0")
}
const pillLinkClass = cn(
"relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
"text-[12px] font-medium text-[#FAFAFA] sm:text-[14px]",
@ -369,6 +390,12 @@ export default function CompanyBrainConnections() {
{ name: string; value: string }[]
>([])
const [customAdvancedOpen, setCustomAdvancedOpen] = useState(false)
const [customAuthMethod, setCustomAuthMethod] = useState<"oauth" | "api-key">(
"oauth",
)
const [directoryOpen, setDirectoryOpen] = useState(false)
const [directoryEntry, setDirectoryEntry] =
useState<McpDirectoryEntry | null>(null)
const { isAdmin } = useOrgMemberRole(isCompanyBrain)
@ -481,22 +508,43 @@ export default function CompanyBrainConnections() {
const resetCustomForm = () => {
setCustomOpen(false)
setDirectoryEntry(null)
setCustomName("")
setCustomServerUrl("")
setCustomToken("")
setCustomHeaderName("")
setCustomExtraHeaders([])
setCustomAdvancedOpen(false)
setCustomAuthMethod("oauth")
}
const setUpDirectoryEntry = (entry: McpDirectoryEntry) => {
setDirectoryEntry(entry)
setCustomName(entry.name)
setCustomServerUrl(entry.url ?? "")
setCustomAdvancedOpen(false)
setCustomAuthMethod(entry.authMethods[0] ?? "oauth")
setCustomOpen(true)
}
const connectCustom = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const slug = slugifyMcpName(customName)
const slug = directoryEntry
? `${slugifyMcpName(directoryEntry.name).slice(0, 49)}-sm-dir-${stableDirectorySuffix(
directoryEntry.url ?? directoryEntry.note ?? directoryEntry.id,
)}`
: slugifyMcpName(customName)
const serverUrl = customServerUrl.trim()
if (!slug) {
toast.error("Enter a custom MCP name.")
return
}
if (!directoryEntry && /-sm-dir-[a-z0-9]{6}$/.test(slug)) {
toast.error(
"Choose a name that doesn't use the reserved directory suffix.",
)
return
}
if (!serverUrl) {
toast.error("Enter an MCP URL.")
return
@ -509,7 +557,11 @@ export default function CompanyBrainConnections() {
const key = `custom:${slug}`
setBusy(key)
try {
const token = customToken.trim()
const token = customAuthMethod === "api-key" ? customToken.trim() : ""
if (customAuthMethod === "api-key" && !token) {
toast.error("Enter an API key.")
return
}
if (token) {
const rows = customExtraHeaders
.map((h) => [h.name.trim(), h.value.trim()] as const)
@ -543,7 +595,7 @@ export default function CompanyBrainConnections() {
toast.error(data.error ?? "Couldn't connect.")
return
}
toast.success(`${slug} connected.`)
toast.success(`${customName} connected.`)
resetCustomForm()
await load()
return
@ -571,7 +623,7 @@ export default function CompanyBrainConnections() {
window.open(data.authUrl, "_blank", "noopener")
resetCustomForm()
} else if (data.ok) {
toast.success(`${slug} connected.`)
toast.success(`${customName} connected.`)
resetCustomForm()
await load()
} else {
@ -696,7 +748,7 @@ export default function CompanyBrainConnections() {
{customRows.map((row) => (
<AppCard
key={`custom-${row.serverSlug}`}
name={titleCase(row.serverSlug.replace(/-/g, " "))}
name={customConnectionName(row.serverSlug)}
subtitle={row.serverUrl ?? "Custom OAuth MCP"}
icon={brainConnectorIcon(row.serverSlug, row.serverSlug)}
userConnected
@ -709,7 +761,7 @@ export default function CompanyBrainConnections() {
disconnect(
{
slug: row.serverSlug,
name: titleCase(row.serverSlug.replace(/-/g, " ")),
name: customConnectionName(row.serverSlug),
category: "Custom OAuth MCP",
authType: "oauth",
},
@ -734,10 +786,36 @@ export default function CompanyBrainConnections() {
)}
</div>
{directoryOpen ? (
<McpDirectoryBrowser
builtInSlugs={catalogSlugs}
onSetUp={setUpDirectoryEntry}
/>
) : (
<button
type="button"
onClick={() => setDirectoryOpen(true)}
className={cn(
dmSans125ClassName(),
"flex w-full cursor-pointer items-center justify-between rounded-xl border border-[#2A313C] border-dashed px-4 py-4 text-left transition-colors hover:border-[#3A4150]",
)}
>
<span>
<span className="block text-[14px] font-semibold text-[#FAFAFA]">
Browse MCP directory
</span>
<span className="mt-0.5 block text-[12px] font-medium text-[#737373]">
Search 654 remote and desktop MCP servers
</span>
</span>
<ChevronDown className="size-4 -rotate-90 text-[#737373]" />
</button>
)}
{/* Reset on every close path so the API key never lingers in state. */}
<Dialog
open={customOpen}
onOpenChange={(open) =>
onOpenChange={(open: boolean) =>
open ? setCustomOpen(true) : resetCustomForm()
}
>
@ -755,11 +833,14 @@ export default function CompanyBrainConnections() {
<div className="flex items-start justify-between gap-4">
<DialogHeader className="flex-1 space-y-1 pl-1">
<DialogTitle className="font-semibold text-[#FAFAFA]">
Add custom connector
{directoryEntry
? `Set up ${directoryEntry.name}`
: "Add custom connector"}
</DialogTitle>
<p className="text-[13px] font-medium leading-[1.35] text-[#737373]">
Connect your Brain to any remote MCP server. Signs in with OAuth
unless you add an API key below.
{directoryEntry?.availability === "tenant"
? "Enter your workspace-specific MCP URL, then choose how this server authenticates."
: "Confirm the remote MCP URL, then choose how this server authenticates."}
</p>
</DialogHeader>
<DialogPrimitive.Close
@ -788,29 +869,66 @@ export default function CompanyBrainConnections() {
className={customInputClass}
/>
<button
type="button"
onClick={() => setCustomAdvancedOpen((open) => !open)}
className="mt-1 flex items-center gap-1.5 self-start text-[13px] font-medium text-[#FAFAFA]"
<div
className={cn(
"grid gap-1 rounded-full bg-[#0D121A] p-1",
directoryEntry?.authMethods.length === 1
? "grid-cols-1"
: "grid-cols-2",
)}
>
<ChevronDown
className={cn(
"size-4 text-[#737373] transition-transform",
customAdvancedOpen && "rotate-180",
)}
/>
Advanced settings
</button>
{(["oauth", "api-key"] as const)
.filter(
(method) =>
!directoryEntry ||
directoryEntry.authMethods.includes(method),
)
.map((method) => (
<button
key={method}
type="button"
onClick={() => setCustomAuthMethod(method)}
className={cn(
"h-8 rounded-full text-[12px] font-semibold transition-colors",
customAuthMethod === method
? "bg-[#252B34] text-[#FAFAFA]"
: "text-[#737373] hover:text-[#D4D4D8]",
)}
>
{method === "oauth" ? "OAuth" : "API key"}
</button>
))}
</div>
{customAdvancedOpen && (
<div className="flex flex-col gap-2">
<input
value={customToken}
onChange={(event) => setCustomToken(event.target.value)}
type="password"
placeholder="API key (optional)"
className={customInputClass}
{customAuthMethod === "api-key" && (
<input
value={customToken}
onChange={(event) => setCustomToken(event.target.value)}
type="password"
placeholder="API key"
required
className={customInputClass}
/>
)}
{customAuthMethod === "api-key" && (
<button
type="button"
onClick={() => setCustomAdvancedOpen((open) => !open)}
className="mt-1 flex items-center gap-1.5 self-start text-[13px] font-medium text-[#FAFAFA]"
>
<ChevronDown
className={cn(
"size-4 text-[#737373] transition-transform",
customAdvancedOpen && "rotate-180",
)}
/>
Header settings
</button>
)}
{customAuthMethod === "api-key" && customAdvancedOpen && (
<div className="flex flex-col gap-2">
<input
value={customHeaderName}
onChange={(event) => setCustomHeaderName(event.target.value)}

View file

@ -0,0 +1,320 @@
"use client"
import { cn } from "@lib/utils"
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"
import { brainConnectorIcon } from "../brain-connector-icons"
const PAGE_SIZE = 48
let directoryCache: McpDirectoryEntry[] | null = null
const AVAILABILITY_LABEL = {
fixed: "Remote URL",
tenant: "Custom URL",
unavailable: "URL unavailable",
local: "Desktop only",
} as const
function isDirectoryEntry(value: unknown): value is McpDirectoryEntry {
if (!value || typeof value !== "object") return false
const entry = value as Partial<McpDirectoryEntry>
return (
typeof entry.id === "string" &&
typeof entry.name === "string" &&
(entry.type === "remote" || entry.type === "local") &&
(entry.url === null || typeof entry.url === "string") &&
typeof entry.auth === "string" &&
(entry.note === null || typeof entry.note === "string") &&
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 ?? "",
)
)
}
function parseDirectory(value: unknown) {
if (!value || typeof value !== "object") throw new Error("invalid catalog")
const entries = (value as { entries?: unknown }).entries
if (!Array.isArray(entries) || !entries.every(isDirectoryEntry)) {
throw new Error("invalid catalog")
}
return entries
}
async function loadDirectory(signal: AbortSignal) {
if (directoryCache) return directoryCache
const response = await fetch("/mcp-directory.json", {
signal,
cache: "default",
})
if (!response.ok) throw new Error("catalog request failed")
directoryCache = parseDirectory(await response.json())
return directoryCache
}
function categoryLabel(value: string) {
return value
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
}
function entrySlug(entry: McpDirectoryEntry) {
return entry.name
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.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 (
<img
src={`/api/mcp-icon?domain=${encodeURIComponent(entry.iconDomain)}`}
alt=""
className="size-5 object-contain"
loading="lazy"
onError={() => setFailed(true)}
/>
)
}
export function McpDirectoryBrowser({
builtInSlugs,
onSetUp,
}: {
builtInSlugs: Set<string>
onSetUp: (entry: McpDirectoryEntry) => void
}) {
const [query, setQuery] = useState("")
const [entries, setEntries] = useState<McpDirectoryEntry[]>([])
const [loadError, setLoadError] = useState(false)
const [category, setCategory] = useState("all")
const [availability, setAvailability] = useState("all")
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
useEffect(() => {
const controller = new AbortController()
void loadDirectory(controller.signal)
.then((data) => {
setEntries(data)
setLoadError(false)
})
.catch((error: unknown) => {
if (error instanceof DOMException && error.name === "AbortError") return
setLoadError(true)
})
return () => controller.abort()
}, [])
const categories = useMemo(
() =>
[...new Set(entries.flatMap((entry) => entry.categories))].sort((a, b) =>
categoryLabel(a).localeCompare(categoryLabel(b)),
),
[entries],
)
const filtered = useMemo(() => {
const needle = query.trim().toLowerCase()
return entries.filter((entry) => {
if (category !== "all" && !entry.categories.includes(category))
return false
if (availability !== "all" && entry.availability !== availability)
return false
if (!needle) return true
return [entry.name, entry.url, entry.note, ...entry.categories]
.filter(Boolean)
.some((value) => value?.toLowerCase().includes(needle))
})
}, [availability, category, entries, query])
const visible = filtered.slice(0, visibleCount)
return (
<section className="space-y-4 pt-3">
<div className="flex flex-col gap-1">
<div className="flex items-baseline justify-between gap-4">
<h2
className={cn(
dmSans125ClassName(),
"font-semibold text-[17px] tracking-[-0.25px] text-[#FAFAFA]",
)}
>
MCP directory
</h2>
<span className="shrink-0 text-[12px] font-medium text-[#737373]">
{entries.length > 0
? `${filtered.length.toLocaleString()} of ${entries.length.toLocaleString()}`
: "654 servers"}
</span>
</div>
<p className="max-w-2xl text-[13px] font-medium leading-5 text-[#737373]">
Browse remote and desktop MCP servers. Setup is available only for
connectors with a verified end-to-end authentication flow.
</p>
</div>
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_180px_180px]">
<label className="relative">
<Search className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[#737373]" />
<input
value={query}
onChange={(event) => {
setQuery(event.target.value)
setVisibleCount(PAGE_SIZE)
}}
placeholder="Search MCPs"
className="h-10 w-full rounded-xl border border-[#252B34] bg-[#111419] pr-3 pl-9 text-[13px] font-medium text-[#FAFAFA] outline-none placeholder:text-[#5F6673] focus:border-[#3A4150]"
/>
</label>
<label className="relative">
<select
aria-label="Filter by category"
value={category}
onChange={(event) => {
setCategory(event.target.value)
setVisibleCount(PAGE_SIZE)
}}
className="h-10 w-full appearance-none rounded-xl border border-[#252B34] bg-[#111419] px-3 pr-8 text-[13px] font-medium text-[#D4D4D8] outline-none focus:border-[#3A4150]"
>
<option value="all">All categories</option>
{categories.map((value) => (
<option key={value} value={value}>
{categoryLabel(value)}
</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-[#737373]" />
</label>
<label className="relative">
<select
aria-label="Filter by availability"
value={availability}
onChange={(event) => {
setAvailability(event.target.value)
setVisibleCount(PAGE_SIZE)
}}
className="h-10 w-full appearance-none rounded-xl border border-[#252B34] bg-[#111419] px-3 pr-8 text-[13px] font-medium text-[#D4D4D8] outline-none focus:border-[#3A4150]"
>
<option value="all">All availability</option>
<option value="fixed">Remote URL</option>
<option value="tenant">Custom URL</option>
<option value="unavailable">URL unavailable</option>
<option value="local">Desktop only</option>
</select>
<ChevronDown className="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-[#737373]" />
</label>
</div>
{loadError ? (
<div className="rounded-xl border border-[#252B34] border-dashed px-4 py-10 text-center text-[13px] font-medium text-[#737373]">
The MCP directory couldn't be loaded. Refresh to try again.
</div>
) : entries.length === 0 ? (
<div className="flex items-center justify-center gap-2 rounded-xl border border-[#252B34] border-dashed px-4 py-10 text-[13px] font-medium text-[#737373]">
<Loader2 className="size-4 animate-spin" />
Loading MCP directory
</div>
) : visible.length > 0 ? (
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{visible.map((entry) => {
const builtIn = builtInSlugs.has(entrySlug(entry))
const canSetUp =
!builtIn &&
entry.setup !== "unsupported" &&
entry.authMethods.length > 0 &&
(entry.availability === "fixed" ||
entry.availability === "tenant")
const subtitle =
entry.categories.length > 0
? entry.categories.map(categoryLabel).join(" · ")
: entry.type === "local"
? "Local desktop extension"
: "Other"
return (
<div
key={entry.id}
className="flex min-w-0 flex-col justify-between gap-3 rounded-xl border border-[#20252D] bg-[#111419] p-3.5 transition-colors hover:border-[#2E3642]"
>
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-[9px] bg-[#080B0F]">
<DirectoryIcon entry={entry} />
</div>
<div className="min-w-0">
<p className="truncate text-[13px] font-semibold text-[#FAFAFA]">
{entry.name}
</p>
<p className="mt-0.5 line-clamp-1 text-[11px] font-medium text-[#737373]">
{subtitle}
</p>
</div>
</div>
<div className="flex items-center justify-between gap-2 border-[#20252D] border-t pt-2.5">
<span className="truncate text-[11px] font-medium text-[#6B7280]">
{setupLabel(entry, builtIn)}
</span>
{canSetUp ? (
<button
type="button"
onClick={() => onSetUp(entry)}
className="shrink-0 cursor-pointer rounded-full bg-[#1B2028] px-3 py-1.5 text-[11px] font-semibold text-[#FAFAFA] transition-colors hover:bg-[#252C37]"
>
Set up
</button>
) : null}
</div>
</div>
)
})}
</div>
) : (
<div className="rounded-xl border border-[#252B34] border-dashed px-4 py-10 text-center text-[13px] font-medium text-[#737373]">
No MCPs match these filters.
</div>
)}
{visibleCount < filtered.length ? (
<button
type="button"
onClick={() => setVisibleCount((count) => count + PAGE_SIZE)}
className="mx-auto flex h-9 cursor-pointer items-center rounded-full border border-[#2A313C] px-5 text-[12px] font-semibold text-[#D4D4D8] transition-colors hover:border-[#3A4150] hover:text-[#FAFAFA]"
>
Show {Math.min(PAGE_SIZE, filtered.length - visibleCount)} more
</button>
) : null}
</section>
)
}

View file

@ -0,0 +1,21 @@
export type McpDirectoryAvailability =
| "fixed"
| "tenant"
| "unavailable"
| "local"
export type McpDirectoryEntry = {
id: string
name: string
type: "remote" | "local"
url: string | null
auth: string
note: string | null
categories: string[]
popularity: number
availability: McpDirectoryAvailability
iconDomain: string | null
setup: "custom" | "unsupported"
oauthCapability: "dcr" | "preregistered" | null
authMethods: Array<"oauth" | "api-key">
}

View file

@ -107,6 +107,6 @@ export default async function proxy(request: Request) {
export const config = {
matcher: [
"/((?!_next/static|_next/image|images|icon.png|favicon.ico|favicon-16x16.png|favicon-32x32.png|apple-touch-icon.png|android-chrome-192x192.png|android-chrome-512x512.png|manifest.webmanifest|site.webmanifest|monitoring|opengraph-image.png|bg-rectangle.png|onboarding|ingest|login|api/emails|mcp-supported-tools|mcp-icon.svg).*)",
"/((?!_next/static|_next/image|images|icon.png|favicon.ico|favicon-16x16.png|favicon-32x32.png|apple-touch-icon.png|android-chrome-192x192.png|android-chrome-512x512.png|manifest.webmanifest|site.webmanifest|monitoring|opengraph-image.png|bg-rectangle.png|onboarding|ingest|login|api/emails|mcp-supported-tools|mcp-icon.svg|mcp-directory.json).*)",
],
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
import argparse
import csv
import hashlib
import json
import subprocess
from urllib.parse import urlparse
from pathlib import Path
ICON_DOMAIN_OVERRIDES = {
"Gmail": "gmail.com",
"Google Calendar": "calendar.google.com",
"Google Drive": "drive.google.com",
"Microsoft 365": "microsoft.com",
"Atlassian Rovo": "atlassian.com",
}
def icon_domain(name: str, url: str | None) -> str | None:
if name in icon_domain_overrides:
return icon_domain_overrides[name]
if url:
parts = (urlparse(url).hostname or "").split(".")
while len(parts) > 2 and parts[0] in {"api", "mcp", "www"}:
parts.pop(0)
return ".".join(parts)
return None
def normalized_url(value: str) -> str:
parsed = urlparse(value)
return parsed._replace(
scheme=parsed.scheme.lower(),
netloc=parsed.netloc.lower(),
path=parsed.path.rstrip("/"),
query="",
fragment="",
).geturl()
parser = argparse.ArgumentParser()
parser.add_argument("input")
parser.add_argument("--output", default="public/mcp-directory.json")
args = parser.parse_args()
with open(Path(__file__).with_name("mcp-oauth-capabilities.json"), encoding="utf-8") as source:
oauth_metadata = json.load(source)
oauth_capabilities = oauth_metadata["capabilities"]
with open(Path(__file__).with_name("mcp-icon-domains.json"), encoding="utf-8") as source:
icon_domain_overrides = {**ICON_DOMAIN_OVERRIDES, **json.load(source)}
entries = []
with open(args.input, newline="", encoding="utf-8-sig") as source:
for index, record in enumerate(csv.DictReader(source), 1):
note = record["note"].strip()
url = record["url"].strip()
entry_type = record["type"].strip()
availability = (
"local"
if entry_type == "local"
else "fixed"
if url
else "tenant"
if note.lower().startswith("per-tenant url")
else "unavailable"
)
name = record["name"].strip()
auth = record["auth"].strip()
oauth_capability = oauth_capabilities.get(normalized_url(url)) if url else None
setup = "unsupported"
entries.append(
{
"id": f"mcp-{index}",
"name": name,
"type": entry_type,
"url": url or None,
"auth": auth,
"note": note or None,
"categories": [value for value in record["category"].split(";") if value],
"popularity": int(record["popularity"] or 0),
"availability": availability,
"iconDomain": icon_domain(name, url or None),
"setup": setup,
"oauthCapability": oauth_capability,
"authMethods": [],
}
)
fingerprint_payload = [
{
"availability": entry["availability"],
"auth": entry["auth"],
"url": normalized_url(entry["url"]) if entry["url"] else "",
}
for entry in entries
]
catalog_fingerprint = hashlib.sha256(
json.dumps(fingerprint_payload, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
if oauth_metadata.get("catalogFingerprint") != catalog_fingerprint:
raise SystemExit("OAuth capability metadata is stale; rerun probe-mcp-oauth.py")
with open(args.output, "w", encoding="utf-8") as target:
json.dump({"version": 1, "entries": entries}, target, ensure_ascii=False, separators=(",", ":"))
subprocess.run(["bunx", "biome", "format", "--write", args.output], check=True)
print(f"Wrote {len(entries)} entries to {args.output}")

View file

@ -0,0 +1,80 @@
{
"10x Genomics Cloud": "10xgenomics.com",
"ActiveCampaign": "activecampaign.com",
"Affinity": "affinity.co",
"Airbnb Search & Listings": "airbnb.com",
"Amplitude": "amplitude.com",
"Android-MCP": "android.com",
"Apify": "apify.com",
"AutoRFP.ai": "autorfp.ai",
"Autodesk Fusion": "autodesk.com",
"Benchling": "benchling.com",
"BlueConic": "blueconic.com",
"Brave (AppleScript)": "brave.com",
"Braze MCP Server": "braze.com",
"Chargebee": "chargebee.com",
"ClickUp": "clickup.com",
"Coralogix": "coralogix.com",
"Coupler.io": "coupler.io",
"Customer.io": "customer.io",
"DataGrail": "datagrail.io",
"Databricks Genie": "databricks.com",
"Datadog": "datadoghq.com",
"Dremio Cloud": "dremio.com",
"Dynatrace MCP Server": "dynatrace.com",
"ElevenLabs": "elevenlabs.io",
"ElevenLabs Agents MCP App": "elevenlabs.io",
"Figma": "figma.com",
"Firefox Control": "firefox.com",
"Funnel": "funnel.io",
"Gainsight (CS)": "gainsight.com",
"Glean": "glean.com",
"Harness MCP Server": "harness.io",
"Intapp Celeste": "intapp.com",
"Ironclad Contracts": "ironcladapp.com",
"JupiterOne": "jupiterone.com",
"Kubernetes MCP Server": "kubernetes.io",
"Litmus MCP": "litmus.com",
"Lumin": "luminpdf.com",
"Metabase": "metabase.com",
"Metabase (Unofficial \u2014 Community)": "metabase.com",
"Microsoft Clarity": "microsoft.com",
"Microsoft Dataverse": "microsoft.com",
"NetSuite": "netsuite.com",
"Ontra": "ontra.ai",
"Pigment": "gopigment.com",
"Rapid7 Bulk Export": "rapid7.com",
"React Aria": "react.dev",
"React Spectrum (S2)": "react.dev",
"Reddit MCP Buddy": "reddit.com",
"Relativity": "relativity.com",
"Retool": "retool.com",
"Revolut X": "revolut.com",
"SAP CAP MCP Server": "sap.com",
"SAP Fiori MCP Server": "sap.com",
"SAP MDK MCP Server": "sap.com",
"ServiceNow": "servicenow.com",
"Sigma": "sigmacomputing.com",
"Sketch": "sketch.com",
"SmartBear MCP": "smartbear.com",
"Smartsheet": "smartsheet.com",
"Snowflake": "snowflake.com",
"Snyk Security": "snyk.io",
"Sourcegraph": "sourcegraph.com",
"Starburst": "starburst.io",
"Tableau": "tableau.com",
"Tines": "tines.com",
"TomTom Maps MCP": "tomtom.com",
"Tray.ai": "tray.io",
"Vendr Software Pricing Tools": "vendr.com",
"Vibe Prospecting": "vibe.com",
"Workato": "workato.com",
"Zoho Analytics": "zoho.com",
"Zoho Books": "zoho.com",
"Zoho CRM": "zoho.com",
"Zoho Desk": "zoho.com",
"Zoho Projects": "zoho.com",
"Zoho Show": "zoho.com",
"Zscaler MCP Server": "zscaler.com",
"n8n": "n8n.io"
}

View file

@ -0,0 +1,289 @@
{
"capabilities": {
"https://agents.riskanalytics.dnb.com/mcp": "preregistered",
"https://ai-inc.mailchimp.com/claude/mcp/v2": "dcr",
"https://ai-tools.tillermoney.com/mcp": "dcr",
"https://ai.thirdbridge.com/mcp/sse": "dcr",
"https://ai.todoist.net/mcp": "dcr",
"https://api-ssl.bitly.com/v4/mcp": "dcr",
"https://api.actively.ai/mcp": "dcr",
"https://api.alphaxiv.org/mcp/v1": "dcr",
"https://api.canary-data.com/mcp": "preregistered",
"https://api.cargoai.co/mcp": "dcr",
"https://api.clarify.ai/mcp": "dcr",
"https://api.clay.com/v3/mcp": "dcr",
"https://api.contentsquare.com/mcp": "dcr",
"https://api.coursera.com/mcp": "preregistered",
"https://api.devrev.ai/mcp/v1": "dcr",
"https://api.dhsprogram.com/mcp": "dcr",
"https://api.digits.com/mcp": "dcr",
"https://api.fathom.ai/mcp": "dcr",
"https://api.fireflies.ai/mcp": "dcr",
"https://api.fitch.group/nexus/mcp": "dcr",
"https://api.fullstory.com/mcp/fullstory": "dcr",
"https://api.fullstory.com/mcp/subtext": "dcr",
"https://api.inductive.bio/mcp": "dcr",
"https://api.isometric.com/mcp": "dcr",
"https://api.latticehq.com/mcp": "preregistered",
"https://api.letsdeel.com/mcp": "dcr",
"https://api.light.inc/mcp": "dcr",
"https://api.meetcampfire.com/mcp": "dcr",
"https://api.meltwater.com/v2/mcp": "dcr",
"https://api.motherduck.com/mcp": "dcr",
"https://api.oreilly.com/api/content-discovery/v1/mcp": "dcr",
"https://api.ottotheagent.com/mcp": "dcr",
"https://api.outreach.io/mcp": "dcr",
"https://api.peec.ai/mcp": "dcr",
"https://api.read.ai/mcp": "dcr",
"https://api.scite.ai/mcp": "dcr",
"https://api.shipbob.com/developer-api/mcp": "dcr",
"https://api.solveintelligence.com/mcp": "preregistered",
"https://api.sumsub.com/mcp": "dcr",
"https://api.udemy.com/mcp": "dcr",
"https://api.wisprflow.ai/connect/mcp": "dcr",
"https://api.within.ai/mcp": "dcr",
"https://app.airops.com/mcp": "dcr",
"https://app.base44.com/mcp": "dcr",
"https://app.brighthire.ai/mcp/v1": "dcr",
"https://app.eraser.io/api/mcp": "dcr",
"https://app.files.com/api/ai/mcp/v1": "dcr",
"https://app.flourish.studio/mcp": "dcr",
"https://app.grasp-ai.com/mcp": "dcr",
"https://app.hanoverpark.com/api/mcp": "dcr",
"https://app.ketryx.com/api/mcp": "dcr",
"https://app.magicschool.ai/api/mcp": "preregistered",
"https://app.midpage.ai/mcp": "dcr",
"https://app.synthesize.bio/api/mcp": "dcr",
"https://app.tropicapp.io/mcp": "dcr",
"https://app.unthread.io/api/mcp": "dcr",
"https://asset-management.mcp.cloudinary.com/sse": "dcr",
"https://bigquery.googleapis.com/mcp": "preregistered",
"https://bindings.mcp.cloudflare.com/mcp": "dcr",
"https://calendarmcp.googleapis.com/mcp/v1": "preregistered",
"https://callbacks.omniapp.co/callback/mcp": "dcr",
"https://circleback.ai/api/mcp": "dcr",
"https://cja-mcp.adobe.io/mcp": "dcr",
"https://compute.googleapis.com/mcp": "preregistered",
"https://coteach.ai/api/mcp": "dcr",
"https://data-search.apigw.feverup.com/mcp": "dcr",
"https://day.ai/api/mcp": "dcr",
"https://docs.superhuman.com/apis/mcp": "dcr",
"https://docuseal.com/mcp": "preregistered",
"https://dovetail.com/api/mcp": "dcr",
"https://drivemcp.googleapis.com/mcp/v1": "preregistered",
"https://elicit.com/api/mcp": "dcr",
"https://fellow.app/mcp": "dcr",
"https://felt.com/mcp": "dcr",
"https://g.runorion.com/mcp": "dcr",
"https://gmailmcp.googleapis.com/mcp/v1": "preregistered",
"https://govcon.dev/mcp": "dcr",
"https://govtribe.com/mcp": "dcr",
"https://grounding.kensho.com/integrations/v2/mcp": "dcr",
"https://haveibeenpwned.com/mcp": "dcr",
"https://huggingface.co/mcp": "dcr",
"https://idiolect.app/api/mcp": "dcr",
"https://ifttt.com/mcp": "dcr",
"https://inkbox.ai/mcp/anthropic": "dcr",
"https://integrators.prod.api.tabsplatform.com/mcp": "dcr",
"https://kfinance.kensho.com/integrations/mcp": "dcr",
"https://legal-mcp.thomsonreuters.com/mcp": "dcr",
"https://legaldatahunter.com/mcp": "dcr",
"https://letsbot.net/mcp": "dcr",
"https://mcp-demo.airwallex.com/developer": "dcr",
"https://mcp-pub.aiera.com": "dcr",
"https://mcp-public.basecamp-research.com/mcp": "dcr",
"https://mcp-server.egnyte.com/mcp": "dcr",
"https://mcp-server.signnow.com/mcp": "dcr",
"https://mcp.adobeaemcloud.com/adobe/mcp/aem": "dcr",
"https://mcp.affinity.co/mcp": "dcr",
"https://mcp.aftership.com/channels": "dcr",
"https://mcp.agentmail.to/mcp": "dcr",
"https://mcp.ai.consilio.com": "dcr",
"https://mcp.alma.food": "dcr",
"https://mcp.alphavantage.co/mcp": "dcr",
"https://mcp.analytics.credit.morningstar.com/mcp": "dcr",
"https://mcp.api.getguru.com/mcp": "dcr",
"https://mcp.api.harmonic.ai": "dcr",
"https://mcp.api.xweather.com/mcp": "dcr",
"https://mcp.apollo.io/mcp": "dcr",
"https://mcp.app.carta.com/mcp": "dcr",
"https://mcp.app.definely.com": "dcr",
"https://mcp.asana.com/v2/mcp": "preregistered",
"https://mcp.ashbyhq.com/mcp/v1": "dcr",
"https://mcp.attio.com/mcp": "dcr",
"https://mcp.auraintelligence.com/mcp": "dcr",
"https://mcp.bigdata.com": "preregistered",
"https://mcp.boltz.bio/mcp": "preregistered",
"https://mcp.box.com": "preregistered",
"https://mcp.brandfetch.io/mcp": "dcr",
"https://mcp.brevo.com/v1/anthropic/mcp": "dcr",
"https://mcp.briskteaching.com/mcp": "dcr",
"https://mcp.calendly.com": "dcr",
"https://mcp.candid.org/mcp": "dcr",
"https://mcp.canva.com/mcp": "dcr",
"https://mcp.cbinsights.com": "dcr",
"https://mcp.chartmogul.com": "dcr",
"https://mcp.clickhouse.cloud/mcp": "dcr",
"https://mcp.clickup.com/mcp": "dcr",
"https://mcp.cloud.cdata.com/mcp": "dcr",
"https://mcp.coindesk.com/mcp": "dcr",
"https://mcp.commonroom.io/mcp": "dcr",
"https://mcp.coupler.io/mcp": "dcr",
"https://mcp.courtlistener.com": "dcr",
"https://mcp.crossbeam.com": "dcr",
"https://mcp.daloopa.com/server/mcp": "dcr",
"https://mcp.datacamp.com/v1/mcp": "dcr",
"https://mcp.deepl.com/v1/mcp": "dcr",
"https://mcp.descrybe.com/mcp": "dcr",
"https://mcp.docusign.com/mcp": "preregistered",
"https://mcp.dropbox.com/claude_app_mcp": "dcr",
"https://mcp.eulerapp.com/mcp": "dcr",
"https://mcp.exa.ai/mcp": "dcr",
"https://mcp.expo.dev/mcp": "dcr",
"https://mcp.factset.com/content/v1": "dcr",
"https://mcp.figma.com/mcp": "dcr",
"https://mcp.financeanalytics.dnb.com/mcp": "preregistered",
"https://mcp.floot.com/mcp": "dcr",
"https://mcp.frontify-integrations.com/mcp": "dcr",
"https://mcp.g2.com/mcp": "preregistered",
"https://mcp.getaugust.ai/mcp": "dcr",
"https://mcp.getmontecarlo.com/mcp": "dcr",
"https://mcp.global.datasite.com/mcp": "dcr",
"https://mcp.gocardless.com": "dcr",
"https://mcp.granola.ai/mcp": "dcr",
"https://mcp.harness.io/mcp": "dcr",
"https://mcp.helium10.com/mcp": "dcr",
"https://mcp.heygen.com/mcp/hyperframes": "dcr",
"https://mcp.highspot.com/mcp": "dcr",
"https://mcp.honeycomb.io/mcp": "dcr",
"https://mcp.ibisworld.com": "dcr",
"https://mcp.imedidata.com/mcp": "dcr",
"https://mcp.indeed.com/claude/mcp": "dcr",
"https://mcp.instrumentl.com": "dcr",
"https://mcp.item.app/mcp": "dcr",
"https://mcp.jam.dev/mcp": "dcr",
"https://mcp.jotform.com/mcp-app": "dcr",
"https://mcp.jotform.com/mcp-jotform-apps-v1": "dcr",
"https://mcp.k.owkin.com/mcp": "preregistered",
"https://mcp.latch.bio/mcp": "dcr",
"https://mcp.lawve.ai/mcp": "dcr",
"https://mcp.leaveadot.com/mcp": "dcr",
"https://mcp.lightfield.app/mcp": "dcr",
"https://mcp.lilt.com/mcp": "dcr",
"https://mcp.linear.app/mcp": "dcr",
"https://mcp.localfalcon.com": "dcr",
"https://mcp.lorikeetcx.ai": "dcr",
"https://mcp.lovable.dev": "dcr",
"https://mcp.lucid.app/mcp": "dcr",
"https://mcp.lumonic.com/mcp": "dcr",
"https://mcp.lusha.com/mcp/claude": "dcr",
"https://mcp.mail.superhuman.com/mcp": "dcr",
"https://mcp.mailerlite.com/mcp": "dcr",
"https://mcp.make.com": "dcr",
"https://mcp.manufact.com/mcp": "dcr",
"https://mcp.mem.ai/mcp": "dcr",
"https://mcp.mercadolibre.com/real-estate/v1/mcp": "dcr",
"https://mcp.mercury.com/mcp": "dcr",
"https://mcp.metal.ai/mcp": "dcr",
"https://mcp.metaview.ai/mcp": "dcr",
"https://mcp.mintlify.com": "dcr",
"https://mcp.miro.com": "dcr",
"https://mcp.mixpanel.com/mcp": "dcr",
"https://mcp.mongodb.com": "dcr",
"https://mcp.moodys.com/genai-ready-data/Credit/mcp": "dcr",
"https://mcp.morningstar.com/mcp": "dcr",
"https://mcp.msci.com/mcp/v1.0/mcp": "dcr",
"https://mcp.mtnewswires.com/mcp": "dcr",
"https://mcp.nimbleway.com/mcp": "dcr",
"https://mcp.notion.com/mcp": "dcr",
"https://mcp.pagerduty.com/mcp": "preregistered",
"https://mcp.pandadoc.com/v1/mcp": "dcr",
"https://mcp.patlytics.ai/mcp": "dcr",
"https://mcp.paytmpayments.com/mcp": "dcr",
"https://mcp.phished.io/mcp": "dcr",
"https://mcp.pi.security/mcp": "dcr",
"https://mcp.pinegap.ai": "dcr",
"https://mcp.plaud.ai/mcp": "dcr",
"https://mcp.posthog.com/mcp": "dcr",
"https://mcp.postman.com/minimal": "dcr",
"https://mcp.privacy.com": "dcr",
"https://mcp.process.st": "dcr",
"https://mcp.prod.originhq.com/mcp": "dcr",
"https://mcp.pscale.dev/mcp/planetscale": "dcr",
"https://mcp.quartr.com/mcp": "dcr",
"https://mcp.quo.com/mcp": "dcr",
"https://mcp.railway.com": "dcr",
"https://mcp.reclaim.ai": "dcr",
"https://mcp.remote.com/mcp": "dcr",
"https://mcp.render.com/mcp": "preregistered",
"https://mcp.resend.com": "dcr",
"https://mcp.roboflow.com/mcp": "dcr",
"https://mcp.salesflare.com/mcp": "dcr",
"https://mcp.sanity.io": "dcr",
"https://mcp.seismic.com/claude": "preregistered",
"https://mcp.sentry.dev/mcp": "dcr",
"https://mcp.shapes.co/mcp": "dcr",
"https://mcp.shippo.com": "dcr",
"https://mcp.similarweb.com": "dcr",
"https://mcp.smartling.com/mcp": "dcr",
"https://mcp.splice.com/mcp": "dcr",
"https://mcp.squareup.com/mcp": "dcr",
"https://mcp.staircase.ai/mcp": "dcr",
"https://mcp.strava.com/mcp": "dcr",
"https://mcp.stripe.com": "dcr",
"https://mcp.sumble.com": "dcr",
"https://mcp.supabase.com/mcp": "dcr",
"https://mcp.sybill.ai/mcp": "dcr",
"https://mcp.synapse.org/mcp": "dcr",
"https://mcp.tavily.com/mcp": "dcr",
"https://mcp.teamtailor.com/mcp": "dcr",
"https://mcp.ticktick.com": "dcr",
"https://mcp.tomtom.com/maps": "dcr",
"https://mcp.trellis.law/anthropic": "dcr",
"https://mcp.trello.com/v1": "dcr",
"https://mcp.tryprofound.com/mcp": "dcr",
"https://mcp.turquoise.health/mcp": "dcr",
"https://mcp.unsplash.com/mcp": "dcr",
"https://mcp.usepylon.com": "dcr",
"https://mcp.vercel.com": "dcr",
"https://mcp.webexapis.com/mcp/webex-meeting": "preregistered",
"https://mcp.webflow.com/mcp": "dcr",
"https://mcp.windsor.ai": "dcr",
"https://mcp.wix.com/mcp": "dcr",
"https://mcp.workable.com/mcp": "dcr",
"https://mcp.workfront.adobe.com/mcp/v1/workfront": "dcr",
"https://mcp.workos.com/mcp": "dcr",
"https://mcp.xero.com/mcp": "preregistered",
"https://mcp.zapier.com/api/v1/connect": "dcr",
"https://mcp.zocks.io/v1/mcp": "dcr",
"https://mcp.zoom.us/mcp/zoom/streamable": "preregistered",
"https://mcp2.readwise.io/mcp": "dcr",
"https://netlify-mcp.netlify.app/mcp": "dcr",
"https://nlp.api.production.unwrap.ai/mcp": "dcr",
"https://playmcp.kakao.com/mcp": "dcr",
"https://premium.mcp.pitchbook.com/mcp": "dcr",
"https://projects.motionapp.com/mcp": "dcr",
"https://public-api.wordpress.com/wpcom/v2/mcp/v1": "dcr",
"https://qbo-connector.meridian.pilot.com/mcp": "dcr",
"https://ramp-mcp-remote.ramp.com/mcp": "dcr",
"https://replit-mcp.com/anthropic/mcp": "dcr",
"https://services.functionhealth.com/ai-chat/mcp": "dcr",
"https://setup.shopify.com/mcp": "preregistered",
"https://sprouts-mcp-server.kartikay-dhar.workers.dev": "dcr",
"https://swagger.mcp.smartbear.com/mcp": "dcr",
"https://teacher-tools.eedi.ai/mcp": "dcr",
"https://tellme.embat.io/mcp": "dcr",
"https://use.kick.co/mcp": "dcr",
"https://v0.app/api/mcp": "dcr",
"https://vast.blueskyapi.com/vianexus/mcp": "dcr",
"https://wisdom-api.enterpret.com/server/mcp": "dcr",
"https://www.legalzoom.com/mcp/claude/v1": "dcr",
"https://www.macaly.com/api/mcp/mcp": "dcr",
"https://www.send.co/mcp": "dcr",
"https://xactrestore-xactremodelserver-usw2-prod.propsol.io/mcp": "dcr"
},
"catalogEntries": 654,
"catalogFingerprint": "62c815b34fd35cd05f0fc5727dfeea21317a438dc66de3e65e8f1f028c8381eb",
"generatedAt": "2026-08-13T01:10:18.235815+00:00",
"probedEntries": 390
}

View file

@ -0,0 +1,156 @@
import argparse
import concurrent.futures
import hashlib
import ipaddress
import json
import socket
import ssl
import urllib.error
import urllib.parse
import urllib.request
from datetime import UTC, datetime
parser = argparse.ArgumentParser()
parser.add_argument("catalog")
parser.add_argument("--output", default="mcp-oauth-capabilities.json")
parser.add_argument("--workers", type=int, default=8)
args = parser.parse_args()
entries = json.load(open(args.catalog, encoding="utf-8"))["entries"]
context = ssl.create_default_context()
def safe_url(url: str) -> bool:
parsed = urllib.parse.urlsplit(url)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
return False
try:
addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
return bool(addresses) and all(
ipaddress.ip_address(address[4][0]).is_global for address in addresses
)
except (OSError, ValueError):
return False
class SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
if not safe_url(new_url):
raise urllib.error.HTTPError(new_url, code, "unsafe redirect", headers, file)
return super().redirect_request(request, file, code, message, headers, new_url)
opener = urllib.request.build_opener(
urllib.request.HTTPSHandler(context=context), SafeRedirectHandler()
)
def get_json(url: str):
if not safe_url(url):
return None
try:
request = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"User-Agent": "supermemory-mcp-directory-audit/1.0",
},
)
with opener.open(request, timeout=6) as response:
if response.status != 200:
return None
value = json.loads(response.read(512_000))
return value if isinstance(value, dict) else None
except Exception:
return None
def normalized_resource(value: str) -> str:
parsed = urllib.parse.urlsplit(value)
return urllib.parse.urlunsplit(
(parsed.scheme.lower(), parsed.netloc.lower(), parsed.path.rstrip("/"), "", "")
)
def catalog_fingerprint(values) -> str:
relevant = [
{
"availability": entry["availability"],
"auth": entry["auth"],
"url": normalized_resource(entry.get("url") or ""),
}
for entry in values
]
payload = json.dumps(relevant, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(payload).hexdigest()
def probe(entry):
if entry["availability"] != "fixed" or entry["auth"] != "auth_required":
return normalized_resource(entry.get("url") or ""), None
parsed = urllib.parse.urlsplit(entry["url"])
origin = f"{parsed.scheme}://{parsed.netloc}"
path = parsed.path.rstrip("/")
protected_resource_urls = []
if path:
protected_resource_urls.append(origin + "/.well-known/oauth-protected-resource" + path)
protected_resource_urls.append(origin + "/.well-known/oauth-protected-resource")
metadata = next(
(value for url in protected_resource_urls if (value := get_json(url))), None
)
if not metadata or normalized_resource(str(metadata.get("resource", ""))) != normalized_resource(entry["url"]):
return normalized_resource(entry["url"]), None
preregistered = False
for server in metadata.get("authorization_servers") or []:
if not isinstance(server, str):
continue
issuer = server.rstrip("/")
issuer_url = urllib.parse.urlsplit(issuer)
candidates = [
issuer + "/.well-known/oauth-authorization-server",
issuer + "/.well-known/openid-configuration",
]
if issuer_url.path and issuer_url.path != "/":
candidates.insert(
0,
f"{issuer_url.scheme}://{issuer_url.netloc}/.well-known/oauth-authorization-server{issuer_url.path}",
)
authorization_metadata = next(
(value for url in candidates if (value := get_json(url))), None
)
if not authorization_metadata:
continue
if authorization_metadata.get("registration_endpoint") or authorization_metadata.get(
"client_id_metadata_document_supported"
):
return normalized_resource(entry["url"]), "dcr"
preregistered = True
return normalized_resource(entry["url"]), "preregistered" if preregistered else None
candidates = [
entry
for entry in entries
if entry["availability"] == "fixed" and entry["auth"] == "auth_required"
]
capabilities = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as executor:
for entry_id, capability in executor.map(probe, candidates):
if capability:
capabilities[entry_id] = capability
output = {
"generatedAt": datetime.now(UTC).isoformat(),
"catalogEntries": len(entries),
"probedEntries": len(candidates),
"catalogFingerprint": catalog_fingerprint(entries),
"capabilities": capabilities,
}
with open(args.output, "w", encoding="utf-8") as target:
json.dump(output, target, indent=2, sort_keys=True)
print(
f"Probed {len(candidates)} entries: "
f"{sum(value == 'dcr' for value in capabilities.values())} DCR, "
f"{sum(value == 'preregistered' for value in capabilities.values())} preregistered"
)