diff --git a/app/.well-known/oauth-protected-resource/route.ts b/app/.well-known/oauth-protected-resource/route.ts new file mode 100644 index 00000000..e1a45a54 --- /dev/null +++ b/app/.well-known/oauth-protected-resource/route.ts @@ -0,0 +1,22 @@ +import { getPublicOrigin } from "mcp-handler"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "authorization, content-type", +}; + +export function GET(request: Request): Response { + return Response.json( + { + resource: `${getPublicOrigin(request)}/api/mcp`, + bearer_methods_supported: ["header"], + scopes_supported: ["publish"], + }, + { headers: corsHeaders }, + ); +} + +export function OPTIONS(): Response { + return new Response(null, { status: 204, headers: corsHeaders }); +} diff --git a/app/[locale]/mcp/McpConnectClient.tsx b/app/[locale]/mcp/McpConnectClient.tsx new file mode 100644 index 00000000..326207ae --- /dev/null +++ b/app/[locale]/mcp/McpConnectClient.tsx @@ -0,0 +1,262 @@ +"use client"; + +import Link from "next/link"; +import { Check, Copy, ExternalLink } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { getStoredToken } from "@/lib/use-auth"; +import { + buildMcpClientSnippets, + MCP_CLIENT_REGISTRY, + type McpClientId, + type McpConnectLocale, + type McpConnectMode, +} from "@/lib/mcp/connect-snippets"; + +interface McpConnectClientProps { + locale: McpConnectLocale; +} + +export function McpConnectClient({ locale }: McpConnectClientProps) { + const t = useTranslations("mcpConnect"); + const [mode, setMode] = useState("publish"); + const [clientId, setClientId] = useState("claude-code"); + const [token, setToken] = useState(null); + const [serverUrl, setServerUrl] = useState(); + const [copyStatus, setCopyStatus] = useState<{ + id: string; + state: "copied" | "failed"; + } | null>(null); + + useEffect(() => { + const storedToken = getStoredToken(); + const currentServerUrl = new URL("/api/mcp", window.location.origin).href; + Promise.resolve().then(() => { + setToken(storedToken); + setServerUrl(currentServerUrl); + }); + }, []); + + const blocks = buildMcpClientSnippets(clientId, { + token, + mode, + locale, + serverUrl, + }); + + async function copy(value: string, id: string) { + try { + await navigator.clipboard.writeText(value); + setCopyStatus({ id, state: "copied" }); + } catch { + setCopyStatus({ id, state: "failed" }); + } + } + + function copyLabel(id: string) { + if (copyStatus?.id !== id) return t("copy.copy"); + return t(`copy.${copyStatus.state}`); + } + + return ( +
+
+

+ {t("eyebrow")} +

+

+ {t("title")} +

+

+ {t("intro")} +

+

+ {t("privacy")} +

+
+ +
+

+ {t("mode.label")} +

+
+ {(["search", "publish"] as const).map((candidate) => ( + + ))} +
+
+ + {token ? ( +
+
+
+ {t("auth.tokenReady")} +
+
+ ••••••••{token.slice(-6)} +
+
+ +
+ ) : ( +
+ {t("auth.loginHint")}{" "} + + {t("auth.loginLink")} + +
+ )} + +
+
+

+ {t("clients.label")} +

+
+ {MCP_CLIENT_REGISTRY.map((client) => ( + + ))} +
+
+ +
+
+

+ {t(`clients.${clientId}`)} +

+
+ +
+ {blocks.map((block) => { + if (block.kind === "code") { + const copyId = `${clientId}-${mode}-${block.id}`; + return ( +
+
+

+ {t(`blocks.${block.title}`)} +

+ {block.detail ? ( + + {block.detail} + + ) : null} +
+
+
+                        {block.content}
+                      
+ +
+
+ ); + } + + if (block.kind === "link") { + return ( + + ); + } + + return ( +

+ {t(`messages.${block.messageKey}`, block.values)} +

+ ); + })} +
+
+
+
+ ); +} diff --git a/app/[locale]/mcp/page.tsx b/app/[locale]/mcp/page.tsx new file mode 100644 index 00000000..62ea060a --- /dev/null +++ b/app/[locale]/mcp/page.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from "next"; +import { hasLocale } from "next-intl"; +import { setRequestLocale } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { Header } from "@/app/components/Header"; +import { Footer } from "@/app/components/Footer"; +import { routing } from "@/i18n/routing"; +import { McpConnectClient } from "./McpConnectClient"; + +export const metadata: Metadata = { + title: "MCP 连接 / MCP Connect · Involution Hell", + description: + "连接 Involution Hell MCP:为 Claude Code、Codex、Cursor、VS Code 等客户端生成可复制的搜索与发布配置。Connect your MCP client with ready-to-copy search and publishing setup.", + alternates: { canonical: "/mcp" }, + openGraph: { + title: "MCP Connect · Involution Hell", + description: + "Ready-to-copy MCP setup for searching and publishing Involution Hell content.", + url: "/mcp", + type: "website", + }, +}; + +interface Props { + params: Promise<{ locale: string }>; +} + +export default async function McpConnectPage({ params }: Props) { + const { locale } = await params; + if (!hasLocale(routing.locales, locale)) notFound(); + setRequestLocale(locale); + + return ( + <> +
+
+ +
+