Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions data/compatibility.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
104 changes: 104 additions & 0 deletions src/app/api/compatibility/route.ts
Original file line number Diff line number Diff line change
@@ -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<string, CompatibilityStatus>;
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<CompatibilitySeedData> {
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" },
}
);
}
Loading
Loading