feat(docs): unified Connectors Catalog (native + live MCP registry) + fix stale stats

New <ConnectorsCatalog/> on /docs/mcp: native connectors (GitHub App / OAuth2 —
GitHub, GitLab, Slack, Google, Discord, X) up top, then the open MCP ecosystem
indexed LIVE from the official registry (registry.modelcontextprotocol.io,
CORS-open, cursor-paginated, 600+ active servers), searchable across both, with
the connect method per entry. Same lazy-island + strip-plugin-allowlist pattern
as ModelsCatalog. One discoverable place for everything Hanzo connects to.

Fix stale landing stats: 157→436 models (the real catalog), 706 'MCP connectors'
→ 600+ MCP servers (the registry we now index), and the prose to match.
This commit is contained in:
Hanzo AI
2026-07-24 15:32:19 -07:00
parent 7d33569cdf
commit bc124f95c4
6 changed files with 183 additions and 7 deletions
+5 -5
View File
@@ -185,8 +185,8 @@ export default function Page() {
<div className="relative mt-14 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-4 gap-px w-full max-w-4xl rounded-2xl overflow-hidden border border-white/[0.08] bg-white/[0.04]">
{[
{ n: '67', label: 'Capabilities' },
{ n: '157', label: 'Models' },
{ n: '706', label: 'MCP connectors' },
{ n: '436', label: 'Models' },
{ n: '600+', label: 'MCP servers' },
{ n: '6', label: 'SDK languages' },
].map((s) => (
<div key={s.label} className="bg-[#0a0a0a] px-5 py-5 text-center">
@@ -324,7 +324,7 @@ print(response.choices[0].message.content)`}
Every model, one API
</h2>
<p className="text-[#737373] text-sm mb-8">
157 models across every major provider call any of them with one credential, one request shape.
436 models across every major provider call any of them with one credential, one request shape.
</p>
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-3">
{[
@@ -375,10 +375,10 @@ print(response.choices[0].message.content)`}
<CardContent className="p-8">
<div className="flex items-center gap-3 mb-2">
<Workflow className="size-5 text-[#a3a3a3]" />
<CardTitle className="text-xl font-bold tracking-tight">706 tools, one MCP surface</CardTitle>
<CardTitle className="text-xl font-bold tracking-tight">Every tool, one MCP surface</CardTitle>
</div>
<CardDescription className="text-xs text-[#525252] mb-6">
706 connectors Slack, GitHub, Notion, Stripe, and more exposed as MCP tools any agent can call.
Native connectors + the open MCP registry Slack, GitHub, Notion, Stripe, and more exposed as MCP tools any agent can call.
</CardDescription>
<div className="flex flex-wrap gap-2">
{['Slack', 'GitHub', 'Notion', 'Stripe', 'Google', 'Linear', '+700 more'].map((c) => (
+162
View File
@@ -0,0 +1,162 @@
'use client';
// Unified connectors catalog — the ONE place to discover everything Hanzo can
// connect to, by how you connect it:
// · Native connectors — first-party integrations you link once (OAuth2 /
// GitHub App) in the console: GitHub, GitLab, Slack, Google, Discord, X …
// · MCP servers — the open Model Context Protocol ecosystem, indexed live
// from the official registry (registry.modelcontextprotocol.io), connect
// over streamable-http or a package. Hundreds of servers, always current.
//
// Rendered inline in the docs (never a link-out). Same lazy-island + fd-token
// pattern as <ModelsCatalog/>.
import { useEffect, useMemo, useState } from 'react';
import { Search, Plug, Boxes, ArrowUpRight, Github, Slack, Chrome, MessageCircle } from 'lucide-react';
const MCP_REGISTRY = 'https://registry.modelcontextprotocol.io/v0/servers?limit=100';
// First-party connectors — linked once in the console, then callable from the
// unified Hanzo MCP + the gateway. method = how you authorize the connection.
type Native = { id: string; label: string; method: string; blurb: string };
const NATIVE: Native[] = [
{ id: 'github', label: 'GitHub', method: 'GitHub App', blurb: 'Repos, issues, PRs, actions, code search.' },
{ id: 'gitlab', label: 'GitLab', method: 'OAuth2', blurb: 'Projects, merge requests, pipelines, issues.' },
{ id: 'slack', label: 'Slack', method: 'OAuth2', blurb: 'Channels, messages, search, notifications.' },
{ id: 'google', label: 'Google', method: 'OAuth2', blurb: 'Drive, Gmail, Calendar, Sheets.' },
{ id: 'discord', label: 'Discord', method: 'OAuth2', blurb: 'Servers, channels, messages.' },
{ id: 'x', label: 'X', method: 'OAuth2', blurb: 'Post, read, and search on X.' },
];
const NATIVE_ICON: Record<string, typeof Github> = { github: Github, slack: Slack, google: Chrome, discord: MessageCircle };
type McpServer = { name: string; title?: string; description?: string; remotes?: { type: string }[]; packages?: { registryType: string }[] };
type RegItem = { server: McpServer };
function connectVia(s: McpServer): string {
if (s.remotes?.length) return s.remotes[0].type; // streamable-http / sse
if (s.packages?.length) return s.packages[0].registryType; // npm / pypi / oci …
return 'mcp';
}
export function ConnectorsCatalog() {
const [servers, setServers] = useState<McpServer[]>([]);
const [cursor, setCursor] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
const [q, setQ] = useState('');
const loadPage = (cur?: string) => {
setLoading(true);
fetch(cur ? `${MCP_REGISTRY}&cursor=${encodeURIComponent(cur)}` : MCP_REGISTRY)
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((d: { servers: RegItem[]; metadata?: { nextCursor?: string } }) => {
setServers((prev) => {
const seen = new Set(prev.map((s) => s.name));
const fresh = d.servers.map((i) => i.server).filter((s) => s.name && !seen.has(s.name));
return [...prev, ...fresh];
});
setCursor(d.metadata?.nextCursor ?? null);
})
.catch((e) => setErr(String(e.message || e)))
.finally(() => setLoading(false));
};
useEffect(() => { loadPage(); /* eslint-disable-next-line */ }, []);
const filtered = useMemo(() => {
const n = q.trim().toLowerCase();
if (!n) return servers;
return servers.filter((s) => `${s.name} ${s.title} ${s.description}`.toLowerCase().includes(n));
}, [servers, q]);
const nativeFiltered = useMemo(() => {
const n = q.trim().toLowerCase();
return n ? NATIVE.filter((c) => `${c.label} ${c.method} ${c.blurb}`.toLowerCase().includes(n)) : NATIVE;
}, [q]);
return (
<div className="not-prose my-6">
<label className="relative mb-5 flex items-center">
<Search className="pointer-events-none absolute left-2.5 size-4 text-fd-muted-foreground" />
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search connectors and MCP servers…"
className="w-full rounded-md border border-fd-border bg-fd-background py-2 pl-8 pr-3 text-sm outline-none focus:border-fd-primary"
/>
</label>
{/* Native connectors */}
{nativeFiltered.length > 0 && (
<section className="mb-8">
<div className="mb-2 flex items-center gap-2">
<Plug className="size-4 text-fd-primary" />
<h3 className="m-0 text-base font-semibold text-fd-foreground">Native connectors</h3>
<span className="text-xs text-fd-muted-foreground">link once in the console</span>
</div>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{nativeFiltered.map((c) => {
const Icon = NATIVE_ICON[c.id] ?? Plug;
return (
<div key={c.id} className="rounded-lg border border-fd-border bg-fd-card p-3">
<div className="flex items-center gap-2">
<Icon className="size-4 text-fd-foreground" />
<span className="font-medium text-fd-foreground">{c.label}</span>
<span className="ml-auto rounded border border-fd-border px-1.5 py-0.5 text-[10px] text-fd-muted-foreground">{c.method}</span>
</div>
<p className="mt-1.5 mb-0 text-sm text-fd-muted-foreground">{c.blurb}</p>
</div>
);
})}
</div>
</section>
)}
{/* MCP servers — live from the official registry */}
<section>
<div className="mb-2 flex items-center gap-2">
<Boxes className="size-4 text-fd-primary" />
<h3 className="m-0 text-base font-semibold text-fd-foreground">MCP servers</h3>
<span className="text-xs text-fd-muted-foreground">
{servers.length ? `${servers.length}+ indexed · Model Context Protocol registry` : 'loading the registry…'}
</span>
</div>
{err ? (
<p className="text-sm text-fd-muted-foreground">
Couldnt reach the MCP registry ({err}). Browse it at{' '}
<a className="text-fd-primary underline" href="https://registry.modelcontextprotocol.io">registry.modelcontextprotocol.io</a>.
</p>
) : (
<>
<div className="overflow-hidden rounded-lg border border-fd-border">
{filtered.map((s, i) => (
<div key={s.name} className={`flex items-start gap-3 p-3 ${i > 0 ? 'border-t border-fd-border/60' : ''}`}>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-fd-foreground">{s.title || s.name}</span>
<span className="rounded border border-fd-border px-1.5 py-0.5 text-[10px] text-fd-muted-foreground">{connectVia(s)}</span>
</div>
<div className="truncate font-mono text-xs text-fd-muted-foreground">{s.name}</div>
{s.description ? <p className="mt-1 mb-0 line-clamp-2 text-sm text-fd-muted-foreground">{s.description}</p> : null}
</div>
</div>
))}
{!filtered.length && !loading ? <div className="p-4 text-sm text-fd-muted-foreground">No servers match {q}.</div> : null}
</div>
{!q && cursor ? (
<button
type="button"
onClick={() => loadPage(cursor)}
disabled={loading}
className="mt-3 inline-flex items-center gap-1.5 rounded-md border border-fd-border px-3 py-1.5 text-sm text-fd-foreground hover:bg-fd-muted disabled:opacity-50"
>
{loading ? 'Loading…' : 'Load more'} <ArrowUpRight className="size-3.5" />
</button>
) : null}
</>
)}
</section>
</div>
);
}
export default ConnectorsCatalog;
+2 -1
View File
@@ -5,7 +5,7 @@ import { Steps, Step } from '@hanzo/docs-ui/components/steps';
import type { MDXComponents } from 'mdx/types';
import { Accordion, Accordions } from '@hanzo/docs-ui/components/accordion';
import { Card, Cards } from '@hanzo/docs-base-ui/components/card';
import { ModelsCatalog } from '@/components/preview/lazy';
import { ModelsCatalog, ConnectorsCatalog } from '@/components/preview/lazy';
import * as icons from 'lucide-react';
export function getMDXComponents(components?: MDXComponents): MDXComponents {
@@ -18,6 +18,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
Cards,
// Live model catalog + pricing (fetches api.hanzo.ai/v1/models at runtime).
ModelsCatalog,
ConnectorsCatalog,
Tabs,
Tab,
TabsContent,
+5
View File
@@ -30,3 +30,8 @@ export const GraphView = dynamic(() =>
export const ModelsCatalog = dynamic(() =>
import('@/components/models-catalog').then((res) => res.ModelsCatalog),
);
// Unified connectors + MCP registry catalog (client-fetches the MCP registry).
export const ConnectorsCatalog = dynamic(() =>
import('@/components/connectors-catalog').then((res) => res.ConnectorsCatalog),
);
+8
View File
@@ -51,6 +51,14 @@ Every tool the platform can expose is indexed at `GET api.hanzo.ai/v1/tools` (au
---
## Connectors
Everything Hanzo can connect to, by how you connect it — native connectors you link once in the console (GitHub App, OAuth2), and the open MCP ecosystem indexed live from the [official registry](https://registry.modelcontextprotocol.io). Search across both.
<ConnectorsCatalog />
---
## MCP as a Service
Beyond the unified server, Hanzo can also **host** any MCP server for you — deploy from any npx, uvx, or custom package with instant scaling and zero infrastructure to manage.
+1 -1
View File
@@ -216,7 +216,7 @@ function transformerEscape(): ShikiTransformer {
// First-party Hanzo components that must survive the unknown-JSX strip in
// services/ + projects/ docs. Only unique names no upstream doc platform ships,
// so allowlisting them can't reintroduce an API collision from ported docs.
const FIRST_PARTY_JSX = new Set(['ModelsCatalog']);
const FIRST_PARTY_JSX = new Set(['ModelsCatalog', 'ConnectorsCatalog']);
function remarkPassthroughUnknownJsx(): Transformer<Root, Root> {
return (tree, file) => {