feat(web): add MCP branding and auth capabilities

This commit is contained in:
Dhravya 2026-08-13 01:42:44 +00:00
parent f6865d33f1
commit c0d38dc137
10 changed files with 4001 additions and 696 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

@ -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}
/>
<div className="grid grid-cols-2 gap-1 rounded-full bg-[#0D121A] p-1">
{(["oauth", "api-key"] as const).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
className={cn(
"grid gap-1 rounded-full bg-[#0D121A] p-1",
directoryEntry?.authMethods.length === 1
? "grid-cols-1"
: "grid-cols-2",
)}
>
{(["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>
{customAuthMethod === "api-key" && (

View file

@ -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 (
<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,
@ -143,9 +182,8 @@ export function McpDirectoryBrowser({
</span>
</div>
<p className="max-w-2xl text-[13px] font-medium leading-5 text-[#737373]">
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.
</p>
</div>
@ -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({
>
<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]">
{entry.type === "local" ? (
<Laptop className="size-4 text-[#A1A1AA]" />
) : (
brainConnectorIcon(entrySlug(entry), entry.name, "size-4")
)}
<DirectoryIcon entry={entry} />
</div>
<div className="min-w-0">
<p className="truncate text-[13px] font-semibold text-[#FAFAFA]">
@ -252,15 +284,7 @@ export function McpDirectoryBrowser({
</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]">
{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)}
</span>
{canSetUp ? (
<button

View file

@ -14,4 +14,8 @@ export type McpDirectoryEntry = {
categories: string[]
popularity: number
availability: McpDirectoryAvailability
iconDomain: string | null
setup: "custom" | "unsupported"
oauthCapability: "dcr" | "preregistered" | null
authMethods: Array<"oauth" | "api-key">
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,51 @@
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):
@ -23,20 +61,42 @@ with open(args.input, newline="", encoding="utf-8-sig") as source:
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": record["name"].strip(),
"name": name,
"type": entry_type,
"url": url or None,
"auth": record["auth"].strip(),
"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=(",", ":"))

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"
)