diff --git a/data/compatibility.json b/data/compatibility.json new file mode 100644 index 0000000..db2e6f0 --- /dev/null +++ b/data/compatibility.json @@ -0,0 +1,180 @@ +{ + "updatedAt": "2026-02-10T00:00:00.000Z", + "clients": [ + { + "id": "claude-desktop", + "name": "Claude Desktop", + "versionRange": ">=0.8" + }, + { + "id": "cursor", + "name": "Cursor", + "versionRange": ">=0.47" + }, + { + "id": "vscode-copilot", + "name": "VS Code Copilot", + "versionRange": ">=1.96" + }, + { + "id": "openclaw", + "name": "OpenClaw", + "versionRange": ">=0.11" + }, + { + "id": "windsurf", + "name": "Windsurf", + "versionRange": ">=1.0" + }, + { + "id": "zed", + "name": "Zed", + "versionRange": ">=0.171" + } + ], + "servers": [ + { + "slug": "filesystem", + "statuses": { + "claude-desktop": "works", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "untested", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Official @modelcontextprotocol server. Baseline file operations are stable in Claude Desktop." + }, + { + "slug": "memory", + "statuses": { + "claude-desktop": "works", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "untested", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Official @modelcontextprotocol server. Known-good for graph memory workflows in Claude Desktop." + }, + { + "slug": "everything", + "statuses": { + "claude-desktop": "works", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "partial", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Official @modelcontextprotocol test server; broad toolset can surface edge-case client bugs." + }, + { + "slug": "sequential-thinking", + "statuses": { + "claude-desktop": "works", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "partial", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Official @modelcontextprotocol server. Multi-step tool calls can be token-budget sensitive in some clients." + }, + { + "slug": "playwright", + "statuses": { + "claude-desktop": "partial", + "cursor": "works", + "vscode-copilot": "partial", + "openclaw": "works", + "windsurf": "partial", + "zed": "untested" + }, + "notes": "Community-maintained by Microsoft. Browser sandboxing constraints vary by client runtime." + }, + { + "slug": "brave-search", + "statuses": { + "claude-desktop": "works", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "works", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Requires Brave API key." + }, + { + "slug": "notion", + "statuses": { + "claude-desktop": "partial", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "partial", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "OAuth/device auth UX differs by host app and can require manual token provisioning." + }, + { + "slug": "postgres", + "statuses": { + "claude-desktop": "untested", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "partial", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Connection-string handling works in OpenClaw with local networking configured." + }, + { + "slug": "desktop-commander", + "statuses": { + "claude-desktop": "untested", + "cursor": "works", + "vscode-copilot": "partial", + "openclaw": "works", + "windsurf": "partial", + "zed": "broken" + }, + "notes": "PTY and shell permission models can fail in constrained desktop clients." + }, + { + "slug": "slack", + "statuses": { + "claude-desktop": "untested", + "cursor": "untested", + "vscode-copilot": "untested", + "openclaw": "partial", + "windsurf": "untested", + "zed": "untested" + }, + "notes": "Bot token scopes and channel permissions are the most common setup issue." + } + ], + "knownIssues": [ + { + "id": "compat-001", + "title": "Zed stdio bridge can drop long-running subprocess output", + "severity": "medium", + "details": "Servers that stream large tool output (for example Desktop Commander) may appear stalled in Zed after 30-60 seconds.", + "affected": ["desktop-commander", "playwright"] + }, + { + "id": "compat-002", + "title": "OAuth callback UX is inconsistent outside Claude Desktop", + "severity": "low", + "details": "Notion/Slack style OAuth flows sometimes require manual token copy-paste in clients without embedded callback handlers.", + "affected": ["notion", "slack"] + }, + { + "id": "compat-003", + "title": "Tool schema edge cases with the everything server", + "severity": "medium", + "details": "Some clients reject nested schema fields used in test fixtures from @modelcontextprotocol/server-everything.", + "affected": ["everything"] + } + ] +} diff --git a/src/app/api/compatibility/route.ts b/src/app/api/compatibility/route.ts new file mode 100644 index 0000000..3ff6204 --- /dev/null +++ b/src/app/api/compatibility/route.ts @@ -0,0 +1,104 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { NextRequest, NextResponse } from "next/server"; +import mcpServers from "@/data/mcp-servers.json"; + +type CompatibilityStatus = "works" | "partial" | "broken" | "untested"; + +type CompatibilityClient = { + id: string; + name: string; + versionRange: string; +}; + +type CompatibilitySeedRow = { + slug: string; + statuses: Record; + notes?: string; +}; + +type CompatibilityKnownIssue = { + id: string; + title: string; + severity: "low" | "medium" | "high"; + details: string; + affected: string[]; +}; + +type CompatibilitySeedData = { + updatedAt: string; + clients: CompatibilityClient[]; + servers: CompatibilitySeedRow[]; + knownIssues: CompatibilityKnownIssue[]; +}; + +type McpServer = { + slug: string; + name: string; + category: string; + repo_url: string; +}; + +const COMPATIBILITY_PATH = path.join(process.cwd(), "data", "compatibility.json"); +const VALID_STATUSES: CompatibilityStatus[] = ["works", "partial", "broken", "untested"]; + +function titleFromSlug(slug: string) { + return slug + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +async function readCompatibilitySeed(): Promise { + const raw = await fs.readFile(COMPATIBILITY_PATH, "utf-8"); + return JSON.parse(raw) as CompatibilitySeedData; +} + +export async function GET(request: NextRequest) { + const clientFilter = request.nextUrl.searchParams.get("client")?.trim(); + const statusFilterRaw = request.nextUrl.searchParams.get("status")?.trim() as CompatibilityStatus | undefined; + const statusFilter = statusFilterRaw && VALID_STATUSES.includes(statusFilterRaw) ? statusFilterRaw : undefined; + + const seed = await readCompatibilitySeed(); + const mcpBySlug = new Map((mcpServers as McpServer[]).map((server) => [server.slug, server])); + + const rows = seed.servers + .map((row) => { + const server = mcpBySlug.get(row.slug); + + return { + slug: row.slug, + name: server?.name ?? titleFromSlug(row.slug), + category: server?.category ?? "unknown", + repoUrl: server?.repo_url ?? null, + notes: row.notes ?? null, + statuses: row.statuses, + }; + }) + .filter((row) => { + if (!statusFilter) return true; + + if (clientFilter) { + return row.statuses[clientFilter] === statusFilter; + } + + return Object.values(row.statuses).some((status) => status === statusFilter); + }); + + return NextResponse.json( + { + updatedAt: seed.updatedAt, + clients: seed.clients, + rows, + knownIssues: seed.knownIssues, + totalRows: rows.length, + filters: { + client: clientFilter ?? null, + status: statusFilter ?? null, + }, + }, + { + headers: { "Cache-Control": "public, max-age=300" }, + } + ); +} diff --git a/src/app/compatibility/compatibility-matrix-client.tsx b/src/app/compatibility/compatibility-matrix-client.tsx new file mode 100644 index 0000000..336a2cb --- /dev/null +++ b/src/app/compatibility/compatibility-matrix-client.tsx @@ -0,0 +1,258 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +type CompatibilityStatus = "works" | "partial" | "broken" | "untested"; + +type CompatibilityClient = { + id: string; + name: string; + versionRange: string; +}; + +type CompatibilityRow = { + slug: string; + name: string; + category: string; + repoUrl: string | null; + notes: string | null; + statuses: Record; +}; + +type CompatibilityKnownIssue = { + id: string; + title: string; + severity: "low" | "medium" | "high"; + details: string; + affected: string[]; +}; + +type CompatibilityResponse = { + updatedAt: string; + clients: CompatibilityClient[]; + rows: CompatibilityRow[]; + knownIssues: CompatibilityKnownIssue[]; +}; + +const STATUS_LABELS: Record = { + works: "✅ Works", + partial: "⚠️ Partial", + broken: "❌ Broken", + untested: "❓ Untested", +}; + +const STATUS_BADGE_STYLES: Record = { + works: "border-emerald-400/30 bg-emerald-500/15 text-emerald-200", + partial: "border-amber-400/30 bg-amber-500/15 text-amber-200", + broken: "border-rose-400/30 bg-rose-500/15 text-rose-200", + untested: "border-slate-500/40 bg-slate-500/10 text-slate-300", +}; + +const ISSUE_SEVERITY_STYLES: Record = { + low: "border-sky-400/30 bg-sky-500/10 text-sky-200", + medium: "border-amber-400/30 bg-amber-500/10 text-amber-200", + high: "border-rose-400/30 bg-rose-500/10 text-rose-200", +}; + +export function CompatibilityMatrixClient() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [clientFilter, setClientFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState<"all" | CompatibilityStatus>("all"); + + useEffect(() => { + const abort = new AbortController(); + + async function loadData() { + try { + setLoading(true); + setError(null); + + const response = await fetch("/api/compatibility", { + method: "GET", + signal: abort.signal, + cache: "no-store", + }); + + if (!response.ok) { + throw new Error("Failed to load compatibility data"); + } + + const payload = (await response.json()) as CompatibilityResponse; + setData(payload); + } catch (err) { + if ((err as Error).name === "AbortError") return; + setError("Could not load compatibility data. Please try again."); + } finally { + setLoading(false); + } + } + + loadData(); + + return () => { + abort.abort(); + }; + }, []); + + const visibleClients = useMemo(() => { + if (!data) return []; + if (clientFilter === "all") return data.clients; + return data.clients.filter((client) => client.id === clientFilter); + }, [clientFilter, data]); + + const filteredRows = useMemo(() => { + if (!data) return []; + + return data.rows.filter((row) => { + if (statusFilter === "all") return true; + + if (clientFilter === "all") { + return Object.values(row.statuses).some((status) => status === statusFilter); + } + + return row.statuses[clientFilter] === statusFilter; + }); + }, [clientFilter, data, statusFilter]); + + if (loading) { + return

Loading compatibility matrix...

; + } + + if (error || !data) { + return

{error ?? "Compatibility data unavailable."}

; + } + + return ( +
+ + + Filters + + +
+
+

Client

+ +
+ +
+

Status

+ +
+
+ +
+

+ Showing {filteredRows.length} of {data.rows.length} servers +

+ +
+
+
+ +
+ + + + + {visibleClients.map((client) => ( + + ))} + + + + {filteredRows.map((row) => ( + + + {visibleClients.map((client) => { + const status = row.statuses[client.id] ?? "untested"; + return ( + + ); + })} + + ))} + +
Server +
{client.name}
+
{client.versionRange}
+
+
{row.name}
+
{row.category}
+ {row.notes ?
{row.notes}
: null} +
+ + {STATUS_LABELS[status]} + +
+
+ + + + Known issues + + + {data.knownIssues.map((issue) => ( +
+
+ + {issue.severity} + +

{issue.title}

+
+

{issue.details}

+

Affected: {issue.affected.join(", ")}

+
+ ))} +
+
+
+ ); +} diff --git a/src/app/compatibility/page.tsx b/src/app/compatibility/page.tsx new file mode 100644 index 0000000..b87d417 --- /dev/null +++ b/src/app/compatibility/page.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { CompatibilityMatrixClient } from "@/app/compatibility/compatibility-matrix-client"; + +export const metadata: Metadata = { + title: "MCP Runtime Compatibility Matrix | forAgents.dev", + description: + "Track MCP server compatibility across Claude Desktop, Cursor, VS Code Copilot, OpenClaw, Windsurf, and Zed.", +}; + +export default function CompatibilityPage() { + return ( +
+
+ + ← Back to MCP Hub + + +
+

+ Runtime Compatibility Matrix +

+

+ A living matrix of MCP server compatibility across major clients and runtime versions. Use filters to view + working, partial, broken, or untested combinations. +

+
+ +
+ +
+
+
+ ); +}