feat: Added McpMarketplaceModal to the CoworkToolCard for improved plugin management.
This commit is contained in:
@@ -4,78 +4,58 @@ import { NextResponse } from "next/server";
|
||||
|
||||
const REGISTRY_URL = "https://api.anthropic.com/mcp-registry/v0/servers";
|
||||
const VISIBILITY = "commercial,gsuite,gsuite-google";
|
||||
const PLUGINS_REPO = "anthropics/knowledge-work-plugins";
|
||||
const GH_API = "https://api.github.com";
|
||||
const GH_RAW = "https://raw.githubusercontent.com";
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
|
||||
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
const G_KEY = "__9routerCoworkMcpRegistryCache";
|
||||
|
||||
function gcache() {
|
||||
if (!globalThis[G_KEY]) globalThis[G_KEY] = { ts: 0, data: null };
|
||||
return globalThis[G_KEY];
|
||||
}
|
||||
|
||||
// Fetch full registry across pagination
|
||||
async function fetchRegistry() {
|
||||
// Filter out claude.ai-mediated servers (broken in 3p) and tenant-required entries.
|
||||
function isDirectConnect(url) {
|
||||
if (!url || typeof url !== "string") return false;
|
||||
if (/^https?:\/\/[^/]*\bmcp\.claude\.com\b/i.test(url)) return false;
|
||||
if (/^https?:\/\/api\.anthropic\.com\/mcp\b/i.test(url)) return false;
|
||||
if (/[<{]/.test(url)) return false;
|
||||
return /^https:\/\//i.test(url);
|
||||
}
|
||||
|
||||
async function fetchAll() {
|
||||
const out = [];
|
||||
let cursor = "";
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const url = `${REGISTRY_URL}?limit=500&visibility=${VISIBILITY}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
|
||||
const r = await fetch(url, { headers: { "accept": "application/json" } });
|
||||
const r = await fetch(url, { headers: { accept: "application/json" } });
|
||||
if (!r.ok) break;
|
||||
const j = await r.json();
|
||||
for (const item of j.servers || []) {
|
||||
const s = item.server || {};
|
||||
const meta = item._meta?.["com.anthropic.api/mcp-registry"] || {};
|
||||
const remote = (s.remotes || [])[0];
|
||||
if (!remote?.url) continue;
|
||||
const transport = remote.type === "streamable-http" ? "http" : (remote.type === "sse" ? "sse" : "http");
|
||||
if (!remote?.url || !isDirectConnect(remote.url)) continue;
|
||||
if (meta.requiredFields?.length) continue;
|
||||
const transport = remote.type === "sse" ? "sse" : "http";
|
||||
const toolNames = Array.isArray(meta.toolNames) ? meta.toolNames : [];
|
||||
out.push({
|
||||
source: "registry",
|
||||
name: s.name,
|
||||
title: s.title || s.name,
|
||||
description: s.description || "",
|
||||
slug: meta.slug || s.name,
|
||||
title: s.title || meta.displayName || s.name,
|
||||
description: s.description || meta.oneLiner || "",
|
||||
url: remote.url,
|
||||
transport,
|
||||
oauth: !meta.isAuthless,
|
||||
toolNames,
|
||||
toolCount: toolNames.length,
|
||||
iconUrl: meta.iconUrl || null,
|
||||
});
|
||||
}
|
||||
cursor = j.metadata?.nextCursor;
|
||||
if (!cursor) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fetch plugins from anthropics/knowledge-work-plugins. Each plugin folder contains
|
||||
// .claude-plugin/plugin.json with mcp_servers map.
|
||||
async function fetchPlugins() {
|
||||
const r = await fetch(`${GH_API}/repos/${PLUGINS_REPO}/contents/`, { headers: { "accept": "application/vnd.github.v3+json" } });
|
||||
if (!r.ok) return [];
|
||||
const items = await r.json();
|
||||
const dirs = items.filter((i) => i.type === "dir" && !i.name.startsWith(".") && i.name !== "partner-built");
|
||||
const out = [];
|
||||
await Promise.all(dirs.map(async (d) => {
|
||||
try {
|
||||
const url = `${GH_RAW}/${PLUGINS_REPO}/main/${d.name}/.claude-plugin/plugin.json`;
|
||||
const pr = await fetch(url);
|
||||
if (!pr.ok) return;
|
||||
const pj = await pr.json();
|
||||
const servers = pj.mcp_servers || pj.mcpServers || {};
|
||||
for (const [key, srv] of Object.entries(servers)) {
|
||||
if (!srv?.url || typeof srv.url !== "string") continue;
|
||||
if (!/^https?:\/\//i.test(srv.url)) continue;
|
||||
const transport = /\/sse(\b|\/)/i.test(srv.url) ? "sse" : (srv.type === "sse" ? "sse" : "http");
|
||||
out.push({
|
||||
source: "plugins",
|
||||
plugin: d.name,
|
||||
name: `${d.name}-${key}`,
|
||||
title: pj.name || d.name,
|
||||
description: pj.description || "",
|
||||
url: srv.url,
|
||||
transport,
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}));
|
||||
return out;
|
||||
// Dedupe by url
|
||||
const seen = new Set();
|
||||
return out.filter((s) => (seen.has(s.url) ? false : (seen.add(s.url), true)));
|
||||
}
|
||||
|
||||
export async function GET(request) {
|
||||
@@ -86,19 +66,12 @@ export async function GET(request) {
|
||||
return NextResponse.json({ cached: true, ...cache.data });
|
||||
}
|
||||
try {
|
||||
const [registry, plugins] = await Promise.all([fetchRegistry(), fetchPlugins()]);
|
||||
// Deduplicate by url
|
||||
const seen = new Set();
|
||||
const merged = [...registry, ...plugins].filter((s) => {
|
||||
if (seen.has(s.url)) return false;
|
||||
seen.add(s.url);
|
||||
return true;
|
||||
});
|
||||
const data = { servers: merged, counts: { registry: registry.length, plugins: plugins.length, total: merged.length } };
|
||||
const servers = await fetchAll();
|
||||
const data = { servers, total: servers.length };
|
||||
cache.ts = Date.now();
|
||||
cache.data = data;
|
||||
return NextResponse.json({ cached: false, ...data });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: e.message, servers: [], counts: { total: 0 } }, { status: 500 });
|
||||
return NextResponse.json({ error: e.message, servers: [], total: 0 }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user