mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-09 08:17:31 +00:00
feat(web): add MCP branding and auth capabilities
This commit is contained in:
parent
f6865d33f1
commit
c0d38dc137
10 changed files with 4001 additions and 696 deletions
61
apps/web/app/api/mcp-icon/route.ts
Normal file
61
apps/web/app/api/mcp-icon/route.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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" && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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=(",", ":"))
|
||||
|
||||
|
|
|
|||
80
apps/web/scripts/mcp-icon-domains.json
Normal file
80
apps/web/scripts/mcp-icon-domains.json
Normal 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"
|
||||
}
|
||||
289
apps/web/scripts/mcp-oauth-capabilities.json
Normal file
289
apps/web/scripts/mcp-oauth-capabilities.json
Normal 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
|
||||
}
|
||||
156
apps/web/scripts/probe-mcp-oauth.py
Normal file
156
apps/web/scripts/probe-mcp-oauth.py
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue