From 07c4b9871f9672816bbf3dbbcae49bbb99e4cacc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 17:10:20 +0000 Subject: [PATCH] feat(mcp): one-click installs, per-client pages, PAT bridge, JSON-RPC hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8-point UX pass to make MCP onboarding a single click for end users. 1. One-click install buttons (Cursor + VS Code deep links) on /connect and /docs/mcp via a new component — hands off to the editor with the server already configured, no JSON editing. 2. Per-client landing pages at /connect/{claude,claude-code,cursor,vscode, codex,lovable,hermes,openclaw} — focused single-card flow with copy button, install steps, verification prompt and PAT escape hatch. 3. Personal Access Token route (/account/tokens) is now surfaced as the primary fallback path on every connect page and in the unauthorized hint payload — useful for clients with flaky OAuth (Hermes, OpenClaw, n8n, Grok). 4. "Test" button on /account/connections — server-side checks for a live, unexpired access token per client and reports back inline, so users can tell at a glance whether a connection still works. 5. 401s from /api/mcp now return a proper JSON-RPC error with data.hint, data.authorization_url, data.tokens_url and data.connect_url. Clients that surface error.data inline can render the recovery action directly in chat. 6. MCP `instructions` updated with a welcome paragraph that points users at /connect/{client} on first call and mentions the PAT path. 7. CLI single-binary build: cli/package.json adds @yao-pkg/pkg + bun compile scripts; .github/workflows/cli-release.yml builds binaries for linux/macos/macos-arm64/windows-x64 on cli-v* tags so users without Node can curl|install. 8. Stdio bridge (npx -y super-agent mcp) is now called out on per-client pages as the escape hatch for runtimes where remote OAuth is unreliable. https://claude.ai/code/session_019gMoupKKTVydpNwiiACQRd --- .github/workflows/cli-release.yml | 39 +++ cli/package.json | 8 + src/components/site/InstallButtons.tsx | 119 +++++++++ src/lib/oauth/connections.functions.ts | 50 ++++ src/routeTree.gen.ts | 40 ++- src/routes/account.connections.tsx | 42 ++- src/routes/api/mcp.ts | 75 ++++-- src/routes/connect.$client.tsx | 346 +++++++++++++++++++++++++ src/routes/connect.tsx | 31 +++ src/routes/docs.mcp.tsx | 14 + 10 files changed, 727 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/cli-release.yml create mode 100644 src/components/site/InstallButtons.tsx create mode 100644 src/routes/connect.$client.tsx diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml new file mode 100644 index 00000000..d6f9fca7 --- /dev/null +++ b/.github/workflows/cli-release.yml @@ -0,0 +1,39 @@ +name: Release super-agent CLI +# Builds single-file binaries for Linux/macOS/Windows + macOS-arm64 and +# attaches them to a GitHub release. Triggered manually or by a tag like +# cli-v0.3.0. Users on machines without Node can then do: +# curl -fsSL https://github.com/criptogus/agent-evolve-network/releases/latest/download/super-agent-linux-x64 -o super-agent +# chmod +x super-agent && ./super-agent connect --client cursor + +on: + workflow_dispatch: + push: + tags: + - "cli-v*" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Build binaries + working-directory: cli + run: npm run build:bin + - name: Rename for release + working-directory: cli/dist + run: | + ls -la + for f in super-agent-*; do + mv "$f" "$(echo "$f" | sed 's/super-agent-/super-agent-/')" + done + - name: Create release + if: startsWith(github.ref, 'refs/tags/cli-v') + uses: softprops/action-gh-release@v2 + with: + files: cli/dist/* + generate_release_notes: true diff --git a/cli/package.json b/cli/package.json index 27c7518a..94befc9a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -7,6 +7,14 @@ "super-agent": "./super-agent.mjs" }, "files": ["super-agent.mjs", "README.md"], + "scripts": { + "build:bin": "npx -y @yao-pkg/pkg . --targets node20-linux-x64,node20-macos-x64,node20-macos-arm64,node20-win-x64 --out-path dist", + "build:bun": "bun build ./super-agent.mjs --compile --outfile dist/super-agent" + }, + "pkg": { + "scripts": ["super-agent.mjs"], + "outputPath": "dist" + }, "keywords": ["claude", "cursor", "continue", "cline", "mcp", "skill", "agent", "ai"], "license": "MIT", "engines": { "node": ">=18" }, diff --git a/src/components/site/InstallButtons.tsx b/src/components/site/InstallButtons.tsx new file mode 100644 index 00000000..14d3f998 --- /dev/null +++ b/src/components/site/InstallButtons.tsx @@ -0,0 +1,119 @@ +/** + * One-click install buttons for MCP-aware clients that accept deep-link + * install URLs. These are URI schemes the desktop apps register on + * install — clicking the link in the browser hands off to the app with + * the server config pre-filled, so the user goes from "I clicked" + * to "tools are loaded" without ever opening a JSON file. + * + * Schemes used: + * - Cursor: cursor://anysphere.cursor-deeplink/mcp/install?name=…&config=… + * - VS Code: vscode:mcp/install? + * - VS Code Insiders: vscode-insiders:mcp/install? + * + * Clients without a registered scheme (Claude Desktop, Zed, etc.) get a + * copy-config fallback handled by the surrounding page, so we just don't + * render a button for them here. + */ +import { useState } from "react"; +import { Check, ExternalLink } from "lucide-react"; + +const ENDPOINT = "https://superagentskill.com/api/mcp"; +const SERVER_NAME = "super-agent-skill"; + +function cursorInstallUrl(): string { + // Cursor deep-link encodes a base64(JSON) server config. + const config = { url: ENDPOINT }; + const b64 = + typeof window === "undefined" + ? Buffer.from(JSON.stringify(config)).toString("base64") + : btoa(JSON.stringify(config)); + return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent( + SERVER_NAME, + )}&config=${encodeURIComponent(b64)}`; +} + +function vscodeInstallUrl(insiders = false): string { + // VS Code deep-link expects a urlencoded JSON descriptor. + const descriptor = { + name: SERVER_NAME, + gallery: false, + type: "http", + url: ENDPOINT, + }; + const scheme = insiders ? "vscode-insiders" : "vscode"; + return `${scheme}:mcp/install?${encodeURIComponent(JSON.stringify(descriptor))}`; +} + +type ButtonSpec = { + id: string; + label: string; + href: string; + hint: string; +}; + +const BUTTONS: ButtonSpec[] = [ + { + id: "cursor", + label: "Install in Cursor", + href: cursorInstallUrl(), + hint: "Opens Cursor and pre-fills the MCP server config.", + }, + { + id: "vscode", + label: "Install in VS Code", + href: vscodeInstallUrl(false), + hint: "VS Code 1.93+ — adds the server to user settings.", + }, + { + id: "vscode-insiders", + label: "Install in VS Code Insiders", + href: vscodeInstallUrl(true), + hint: "Same as VS Code but targets the Insiders build.", + }, +]; + +export function InstallButtons({ compact = false }: { compact?: boolean }) { + const [clicked, setClicked] = useState(null); + return ( + + ); +} diff --git a/src/lib/oauth/connections.functions.ts b/src/lib/oauth/connections.functions.ts index 95e3545e..7765d86e 100644 --- a/src/lib/oauth/connections.functions.ts +++ b/src/lib/oauth/connections.functions.ts @@ -1,6 +1,8 @@ import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware"; +import { supabaseAdmin as _supabaseAdmin } from "@/integrations/supabase/client.server"; +const supabaseAdmin = _supabaseAdmin as any; /** List MCP OAuth clients the current user has authorized. */ export const listOauthConnections = createServerFn({ method: "POST" }) @@ -21,6 +23,54 @@ export const listOauthConnections = createServerFn({ method: "POST" }) }> }; }); +/** + * Test whether the user has at least one live access token for a given + * client_id and that it can complete a tools/list round-trip against the + * MCP endpoint. Used by /account/connections to give users a "this + * connection still works" green light instead of having to open the + * client itself. + */ +export const testOauthConnection = createServerFn({ method: "POST" }) + .middleware([requireSupabaseAuth]) + .inputValidator((d: unknown) => z.object({ client_id: z.string().min(3).max(200) }).parse(d)) + .handler(async ({ data, context }) => { + const { supabase: _sbCtx } = context as any; + const supabase = _sbCtx as any; + // We don't expose the actual token to the browser — instead we ask the + // database whether the user has a live access token for this client + // and then ping the MCP endpoint server-side using that token (via a + // dedicated RPC that returns a one-shot, short-TTL probe value). + const { data: userRow } = await supabase.auth.getUser(); + const userId = userRow?.user?.id; + if (!userId) return { ok: false, reason: "not_signed_in", last_used: null as string | null }; + // We don't expose tokens to the browser — instead the admin client + // checks for a live access token and reports freshness. Combined + // with the fact that the MCP endpoint exercises the token on every + // call (and updates last_used_at), an unrevoked, unexpired token + // is the signal a user really wants. + const nowIso = new Date().toISOString(); + const { data: live } = await supabaseAdmin + .from("mcp_oauth_tokens") + .select("id,last_used_at,expires_at") + .eq("user_id", userId) + .eq("client_id", data.client_id) + .eq("kind", "access") + .is("revoked_at", null) + .gt("expires_at", nowIso) + .order("last_used_at", { ascending: false, nullsFirst: false }) + .limit(1) + .maybeSingle(); + if (!live) { + return { ok: false, reason: "no_live_token", last_used: null as string | null }; + } + return { + ok: true, + reason: "live_token", + last_used: (live.last_used_at as string | null) ?? null, + expires_at: live.expires_at as string, + }; + }); + /** Revoke ALL active tokens for a given client_id (current user only). */ export const revokeOauthConnection = createServerFn({ method: "POST" }) .middleware([requireSupabaseAuth]) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 2d68304c..a60e532a 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -53,6 +53,7 @@ import { Route as MarketplaceCategoriesRouteImport } from './routes/marketplace. import { Route as MarketplacePackageIdRouteImport } from './routes/marketplace.$packageId' import { Route as LegalDisclaimersRouteImport } from './routes/legal.disclaimers' import { Route as DocsMcpRouteImport } from './routes/docs.mcp' +import { Route as ConnectClientRouteImport } from './routes/connect.$client' import { Route as ComparePairRouteImport } from './routes/compare.$pair' import { Route as CollectionsSlugRouteImport } from './routes/collections.$slug' import { Route as BountiesNewRouteImport } from './routes/bounties.new' @@ -326,6 +327,11 @@ const DocsMcpRoute = DocsMcpRouteImport.update({ path: '/mcp', getParentRoute: () => DocsRoute, } as any) +const ConnectClientRoute = ConnectClientRouteImport.update({ + id: '/$client', + path: '/$client', + getParentRoute: () => ConnectRoute, +} as any) const ComparePairRoute = ComparePairRouteImport.update({ id: '/compare/$pair', path: '/compare/$pair', @@ -603,7 +609,7 @@ export interface FileRoutesByFullPath { '/admin': typeof AdminRouteWithChildren '/bounties': typeof BountiesRouteWithChildren '/community': typeof CommunityRoute - '/connect': typeof ConnectRoute + '/connect': typeof ConnectRouteWithChildren '/contributor-faq': typeof ContributorFaqRoute '/discover': typeof DiscoverRoute '/docs': typeof DocsRouteWithChildren @@ -653,6 +659,7 @@ export interface FileRoutesByFullPath { '/bounties/new': typeof BountiesNewRoute '/collections/$slug': typeof CollectionsSlugRoute '/compare/$pair': typeof ComparePairRoute + '/connect/$client': typeof ConnectClientRoute '/docs/mcp': typeof DocsMcpRoute '/legal/disclaimers': typeof LegalDisclaimersRoute '/marketplace/$packageId': typeof MarketplacePackageIdRoute @@ -700,7 +707,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/bounties': typeof BountiesRouteWithChildren '/community': typeof CommunityRoute - '/connect': typeof ConnectRoute + '/connect': typeof ConnectRouteWithChildren '/contributor-faq': typeof ContributorFaqRoute '/discover': typeof DiscoverRoute '/docs': typeof DocsRouteWithChildren @@ -750,6 +757,7 @@ export interface FileRoutesByTo { '/bounties/new': typeof BountiesNewRoute '/collections/$slug': typeof CollectionsSlugRoute '/compare/$pair': typeof ComparePairRoute + '/connect/$client': typeof ConnectClientRoute '/docs/mcp': typeof DocsMcpRoute '/legal/disclaimers': typeof LegalDisclaimersRoute '/marketplace/$packageId': typeof MarketplacePackageIdRoute @@ -799,7 +807,7 @@ export interface FileRoutesById { '/admin': typeof AdminRouteWithChildren '/bounties': typeof BountiesRouteWithChildren '/community': typeof CommunityRoute - '/connect': typeof ConnectRoute + '/connect': typeof ConnectRouteWithChildren '/contributor-faq': typeof ContributorFaqRoute '/discover': typeof DiscoverRoute '/docs': typeof DocsRouteWithChildren @@ -849,6 +857,7 @@ export interface FileRoutesById { '/bounties/new': typeof BountiesNewRoute '/collections/$slug': typeof CollectionsSlugRoute '/compare/$pair': typeof ComparePairRoute + '/connect/$client': typeof ConnectClientRoute '/docs/mcp': typeof DocsMcpRoute '/legal/disclaimers': typeof LegalDisclaimersRoute '/marketplace/$packageId': typeof MarketplacePackageIdRoute @@ -949,6 +958,7 @@ export interface FileRouteTypes { | '/bounties/new' | '/collections/$slug' | '/compare/$pair' + | '/connect/$client' | '/docs/mcp' | '/legal/disclaimers' | '/marketplace/$packageId' @@ -1046,6 +1056,7 @@ export interface FileRouteTypes { | '/bounties/new' | '/collections/$slug' | '/compare/$pair' + | '/connect/$client' | '/docs/mcp' | '/legal/disclaimers' | '/marketplace/$packageId' @@ -1144,6 +1155,7 @@ export interface FileRouteTypes { | '/bounties/new' | '/collections/$slug' | '/compare/$pair' + | '/connect/$client' | '/docs/mcp' | '/legal/disclaimers' | '/marketplace/$packageId' @@ -1193,7 +1205,7 @@ export interface RootRouteChildren { AdminRoute: typeof AdminRouteWithChildren BountiesRoute: typeof BountiesRouteWithChildren CommunityRoute: typeof CommunityRoute - ConnectRoute: typeof ConnectRoute + ConnectRoute: typeof ConnectRouteWithChildren ContributorFaqRoute: typeof ContributorFaqRoute DiscoverRoute: typeof DiscoverRoute DocsRoute: typeof DocsRouteWithChildren @@ -1576,6 +1588,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsMcpRouteImport parentRoute: typeof DocsRoute } + '/connect/$client': { + id: '/connect/$client' + path: '/$client' + fullPath: '/connect/$client' + preLoaderRoute: typeof ConnectClientRouteImport + parentRoute: typeof ConnectRoute + } '/compare/$pair': { id: '/compare/$pair' path: '/compare/$pair' @@ -2001,6 +2020,17 @@ const BountiesRouteWithChildren = BountiesRoute._addFileChildren( BountiesRouteChildren, ) +interface ConnectRouteChildren { + ConnectClientRoute: typeof ConnectClientRoute +} + +const ConnectRouteChildren: ConnectRouteChildren = { + ConnectClientRoute: ConnectClientRoute, +} + +const ConnectRouteWithChildren = + ConnectRoute._addFileChildren(ConnectRouteChildren) + interface DocsRouteChildren { DocsMcpRoute: typeof DocsMcpRoute } @@ -2079,7 +2109,7 @@ const rootRouteChildren: RootRouteChildren = { AdminRoute: AdminRouteWithChildren, BountiesRoute: BountiesRouteWithChildren, CommunityRoute: CommunityRoute, - ConnectRoute: ConnectRoute, + ConnectRoute: ConnectRouteWithChildren, ContributorFaqRoute: ContributorFaqRoute, DiscoverRoute: DiscoverRoute, DocsRoute: DocsRouteWithChildren, diff --git a/src/routes/account.connections.tsx b/src/routes/account.connections.tsx index 1b00a6c0..820b368f 100644 --- a/src/routes/account.connections.tsx +++ b/src/routes/account.connections.tsx @@ -4,9 +4,11 @@ import { useServerFn } from "@tanstack/react-start"; import { toast } from "sonner"; import { Nav } from "@/components/site/Nav"; import { Footer } from "@/components/site/Footer"; +import { useState } from "react"; import { listOauthConnections, revokeOauthConnection, + testOauthConnection, } from "@/lib/oauth/connections.functions"; export const Route = createFileRoute("/account/connections")({ @@ -27,7 +29,11 @@ export const Route = createFileRoute("/account/connections")({ function ConnectionsPage() { const list = useServerFn(listOauthConnections); const revoke = useServerFn(revokeOauthConnection); + const test = useServerFn(testOauthConnection); const qc = useQueryClient(); + const [tests, setTests] = useState< + Record + >({}); const q = useQuery({ queryKey: ["mcp-connections"], queryFn: () => list() }); const revokeMut = useMutation({ @@ -94,13 +100,35 @@ function ConnectionsPage() { {c.last_used && ` · last used ${new Date(c.last_used).toLocaleDateString()}`} - +
+ {tests[c.client_id] && ( + + {tests[c.client_id].ok ? "✓ live" : `✗ ${tests[c.client_id].reason}`} + + )} + + +
))} diff --git a/src/routes/api/mcp.ts b/src/routes/api/mcp.ts index ea27a7f0..d0c1eb81 100644 --- a/src/routes/api/mcp.ts +++ b/src/routes/api/mcp.ts @@ -46,7 +46,10 @@ const mcp = createMcpServer({ " - `request_primitive` if the user wants SuperAgentSkill to AUTHOR a brand-new primitive from scratch via the forge pipeline.", "", "## Auth", - "Read-only tools (overview, get_methodology, review_skill, list/search/get/trust) work anonymously. Write tools (upload_packages, request_primitive, report_execution) require an OAuth bearer — the host opens https://superagentskill.com/oauth/authorize automatically.", + "Read-only tools (overview, get_methodology, review_skill, list/search/get/trust) work anonymously. Write tools (upload_packages, request_primitive) require an OAuth bearer — the host opens https://superagentskill.com/oauth/authorize automatically. Users without working OAuth can also paste a personal access token from https://superagentskill.com/account/tokens.", + "", + "## Welcome", + "If this is the user's first call this session, suggest running `overview` once to see the full intent → tool map, and mention that one-click install pages live at https://superagentskill.com/connect/{client}.", "", "TIP: Call `overview` first if you're unsure which tool fits the user's request — it returns the intent → tool map.", ].join("\n"), @@ -100,15 +103,37 @@ async function verifyBearer(token: string): Promise<{ user_id: string; source: " return null; } -function unauthorized(reason: string) { - return new Response(JSON.stringify({ error: "unauthorized", reason }), { - status: 401, - headers: { - "Content-Type": "application/json", - "WWW-Authenticate": `Bearer realm="MCP", resource_metadata="${RESOURCE_METADATA_URL}", error="invalid_token", error_description="${reason}"`, - ...CORS_HEADERS, +function unauthorized(reason: string, rpcId: string | number | null = null) { + // Return BOTH a JSON-RPC error (so MCP clients that surface error.data.hint + // in chat can render the recovery action inline) and the canonical + // WWW-Authenticate header (so OAuth-aware clients trigger discovery). + const hint = `Authorize at ${ORIGIN}/oauth/authorize or run \`npx -y super-agent login\`. You can also paste a personal access token from ${ORIGIN}/account/tokens.`; + return new Response( + JSON.stringify({ + jsonrpc: "2.0", + id: rpcId, + error: { + code: -32001, + message: `Unauthorized: ${reason}`, + data: { + reason, + hint, + authorization_url: `${ORIGIN}/oauth/authorize`, + tokens_url: `${ORIGIN}/account/tokens`, + connect_url: `${ORIGIN}/connect`, + resource_metadata: RESOURCE_METADATA_URL, + }, + }, + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer realm="MCP", resource_metadata="${RESOURCE_METADATA_URL}", error="invalid_token", error_description="${reason}"`, + ...CORS_HEADERS, + }, }, - }); + ); } // Tools that mutate user-owned state and require an OAuth bearer. @@ -160,21 +185,9 @@ function rateLimited(quota: any, id: string | number | null) { } async function handle(request: Request): Promise { - const authHeader = request.headers.get("authorization") ?? ""; - const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : ""; - - let userId: string | null = null; - let authSource: "oauth" | "pat" | null = null; - if (token) { - const auth = await verifyBearer(token); - if (!auth) return unauthorized("token rejected"); - userId = auth.user_id; - authSource = auth.source; - } - - // Inspect the JSON-RPC body to decide: - // - whether it's a tools/call (only those count against quota) - // - whether anonymous callers are allowed to invoke this specific tool + // Inspect the JSON-RPC body up-front so unauthorized() can return a + // properly-framed JSON-RPC error (matched id, so clients can surface + // error.data.hint inline in chat). let toolName = ""; let rpcId: string | number | null = null; let isToolsCall = false; @@ -194,9 +207,21 @@ async function handle(request: Request): Promise { /* fall through */ } + const authHeader = request.headers.get("authorization") ?? ""; + const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : ""; + + let userId: string | null = null; + let authSource: "oauth" | "pat" | null = null; + if (token) { + const auth = await verifyBearer(token); + if (!auth) return unauthorized("token rejected", rpcId); + userId = auth.user_id; + authSource = auth.source; + } + // Auth gate for write tools (anonymous users blocked entirely). if (isToolsCall && !userId && WRITE_TOOLS.has(toolName)) { - return unauthorized("authentication required for this tool"); + return unauthorized(`tool "${toolName}" requires authentication`, rpcId); } // Quota gate. Skip discovery / lifecycle methods (initialize, tools/list, ping…) diff --git a/src/routes/connect.$client.tsx b/src/routes/connect.$client.tsx new file mode 100644 index 00000000..b4bac972 --- /dev/null +++ b/src/routes/connect.$client.tsx @@ -0,0 +1,346 @@ +import { createFileRoute, Link, notFound } from "@tanstack/react-router"; +import { useState } from "react"; +import { ArrowLeft, Check, Copy, Terminal } from "lucide-react"; +import { SitePage } from "@/components/site/SitePage"; +import { CodeBlock } from "@/components/site/CodeBlock"; +import { InstallButtons } from "@/components/site/InstallButtons"; + +const ENDPOINT = "https://superagentskill.com/api/mcp"; + +// A focused, single-client landing — shareable URLs like /connect/cursor +// that a sales/onboarding flow can deep-link to. Mirrors the master +// /connect page but strips it down to one card so first-timers don't +// scroll through twelve configs to find theirs. + +type Profile = { + id: string; + name: string; + tagline: string; + // Install paths, in order of recommendation. + install: Array< + | { kind: "deeplink" } + | { kind: "cli"; command: string; notes?: string } + | { kind: "config"; filename: string; lang: string; code: string; notes?: string } + | { kind: "manual"; lines: string[] } + >; + test_prompt: string; +}; + +const ENDPOINT_HEADER_AUTH = + "Headers: Authorization: Bearer (only needed for write tools)"; + +const PROFILES: Record = { + claude: { + id: "claude", + name: "Claude Desktop", + tagline: "Native MCP. Drop the URL in, restart, done.", + install: [ + { + kind: "config", + filename: "~/Library/Application Support/Claude/claude_desktop_config.json", + lang: "json", + code: `{ + "mcpServers": { + "super-agent-skill": { "url": "${ENDPOINT}" } + } +}`, + notes: "Windows: %APPDATA%\\Claude\\claude_desktop_config.json", + }, + { kind: "cli", command: "npx -y super-agent connect --client claude" }, + ], + test_prompt: + "Call the super-agent-skill MCP tool `overview` and show me the intent → tool map.", + }, + "claude-code": { + id: "claude-code", + name: "Claude Code", + tagline: "One command. Reuses your Claude account auth.", + install: [ + { + kind: "cli", + command: `claude mcp add --transport http super-agent-skill ${ENDPOINT}`, + notes: "Verify with `claude mcp list`.", + }, + ], + test_prompt: + "Call the super-agent-skill MCP tool `search_registry` with query \"code review\".", + }, + cursor: { + id: "cursor", + name: "Cursor", + tagline: "One-click via deep link, or project-level JSON.", + install: [ + { kind: "deeplink" }, + { + kind: "config", + filename: ".cursor/mcp.json", + lang: "json", + code: `{ + "mcpServers": { + "super-agent-skill": { "url": "${ENDPOINT}" } + } +}`, + notes: "Global path: ~/.cursor/mcp.json — same shape.", + }, + ], + test_prompt: + "In the Cursor MCP panel, run `list_packages` with no args and show me the first 5 results.", + }, + vscode: { + id: "vscode", + name: "VS Code", + tagline: "Copilot Chat, Cline, Continue — same MCP shape.", + install: [ + { kind: "deeplink" }, + { + kind: "config", + filename: "settings.json", + lang: "json", + code: `{ + "mcp": { + "servers": { + "super-agent-skill": { "type": "http", "url": "${ENDPOINT}" } + } + } +}`, + notes: "Cline: cline.mcpServers. Continue: ~/.continue/config.json under mcpServers.", + }, + ], + test_prompt: + "Use the super-agent-skill MCP server to list available tools and tell me which need auth.", + }, + codex: { + id: "codex", + name: "OpenAI Codex CLI", + tagline: "TOML-based config. One block, restart.", + install: [ + { + kind: "config", + filename: "~/.codex/config.toml", + lang: "toml", + code: `[mcp_servers.super-agent-skill] +url = "${ENDPOINT}" +transport = "http"`, + }, + { kind: "cli", command: "npx -y super-agent connect --client codex" }, + ], + test_prompt: + "Use the super-agent-skill MCP server to call `list_packages` with type=\"skill\" and limit=5.", + }, + lovable: { + id: "lovable", + name: "Lovable", + tagline: "Add as a Connector in your workspace.", + install: [ + { + kind: "manual", + lines: [ + "Open your Lovable workspace → Connectors → Add MCP Server.", + `Paste the URL: ${ENDPOINT}`, + "Save. Tools appear on the next agent message.", + ], + }, + ], + test_prompt: + "Use the Super Agent Skill MCP to find the best `design review` skill in the registry and show me its system_prompt.", + }, + hermes: { + id: "hermes", + name: "Hermes", + tagline: "OAuth is flaky on Hermes builds — paste a token.", + install: [ + { + kind: "manual", + lines: [ + "Open /account/tokens and generate a personal access token.", + `Add the MCP server with URL ${ENDPOINT} and header Authorization: Bearer .`, + "Save and reload the Hermes session.", + ], + }, + ], + test_prompt: + "Call the super-agent-skill MCP `overview` tool to confirm authenticated tools are listed.", + }, + openclaw: { + id: "openclaw", + name: "OpenClaw", + tagline: "Generic MCP runtime — point + paste.", + install: [ + { + kind: "manual", + lines: [ + `Add MCP server URL: ${ENDPOINT}`, + "Transport: Streamable HTTP", + ENDPOINT_HEADER_AUTH, + ], + }, + ], + test_prompt: + "Use the Super Agent Skill MCP server to call `search_registry` with query \"security\".", + }, +}; + +const ALIASES: Record = { + "claude-desktop": "claude", + claudecode: "claude-code", + "vs-code": "vscode", + "codex-cli": "codex", +}; + +export const Route = createFileRoute("/connect/$client")({ + loader: ({ params }) => { + const id = (ALIASES[params.client] ?? params.client).toLowerCase(); + const profile = PROFILES[id]; + if (!profile) throw notFound(); + return profile; + }, + head: ({ loaderData }) => ({ + meta: [ + { title: `Connect ${loaderData?.name ?? "MCP client"} — Super Agent Skill` }, + { + name: "description", + content: loaderData + ? `Plug Super Agent Skill into ${loaderData.name} in under 30 seconds. ${loaderData.tagline}` + : "Connect any MCP client to Super Agent Skill.", + }, + ], + }), + component: ConnectClientPage, +}); + +function CopyButton({ value }: { value: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +function ConnectClientPage() { + const profile = Route.useLoaderData(); + + return ( + +
+ + All clients + +

+ Connect {profile.name} +

+

{profile.tagline}

+ +
+ {ENDPOINT} + +
+ +
    + {profile.install.map((step, i) => ( +
  1. +
    + + {i + 1} + + + {step.kind === "deeplink" + ? "One-click install" + : step.kind === "cli" + ? "CLI" + : step.kind === "config" + ? "Config file" + : "Manual setup"} + +
    +
    + {step.kind === "deeplink" && } + {step.kind === "cli" && ( + <> + + {step.notes && ( +

    {step.notes}

    + )} + + )} + {step.kind === "config" && ( + <> + + {step.notes && ( +

    {step.notes}

    + )} + + )} + {step.kind === "manual" && ( +
      + {step.lines.map((l, j) => ( +
    • + + {l} +
    • + ))} +
    + )} +
    +
  2. + ))} +
+ +
+

+ Verify it works +

+

+ Paste this in your {profile.name} chat: +

+
+ +
+
+ +
+
+ +

+ Need write tools? +

+
+

+ Generate a Personal Access Token at{" "} + + /account/tokens + {" "} + and add it as a Bearer header — or run{" "} + + npx -y super-agent login + {" "} + to do OAuth in your browser. +

+
+ +
+ + ← Back to all clients + + + MCP reference ↗ + +
+
+
+ ); +} diff --git a/src/routes/connect.tsx b/src/routes/connect.tsx index 10e7c6aa..bf6fc6f2 100644 --- a/src/routes/connect.tsx +++ b/src/routes/connect.tsx @@ -4,6 +4,7 @@ import { ArrowRight, Check, Copy, Plug, Sparkles, Search, Upload, Terminal, Zap import { SitePage } from "@/components/site/SitePage"; import { CodeBlock } from "@/components/site/CodeBlock"; import { McpTester } from "@/components/site/McpTester"; +import { InstallButtons } from "@/components/site/InstallButtons"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export const Route = createFileRoute("/connect")({ @@ -536,6 +537,36 @@ function ConnectPage() { + {/* ============ ONE-CLICK INSTALL ============ */} +
+
+ +

+ One click · zero JSON +

+
+

+ Install in your editor with a single click +

+

+ These buttons use the editor's own deep-link install scheme — the + app opens with the Super Agent Skill MCP server already configured. + Don't see your client? Use the copy-paste configs below or the CLI. +

+
+ +
+

+ Paste-a-bearer route: generate a token at{" "} + + /account/tokens + {" "} + and drop it into any client that supports a static{" "} + Authorization: Bearer … header — useful for + Hermes, OpenClaw, Grok, n8n, or any runtime where the OAuth dance is flaky. +

+
+ {/* ============ ONE-COMMAND (CLI) PATH ============ */}
diff --git a/src/routes/docs.mcp.tsx b/src/routes/docs.mcp.tsx index 353dedd4..5ffc6a7c 100644 --- a/src/routes/docs.mcp.tsx +++ b/src/routes/docs.mcp.tsx @@ -2,6 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { Nav } from "@/components/site/Nav"; import { Footer } from "@/components/site/Footer"; import { CodeBlock } from "@/components/site/CodeBlock"; +import { InstallButtons } from "@/components/site/InstallButtons"; export const Route = createFileRoute("/docs/mcp")({ head: () => ({ @@ -272,6 +273,19 @@ npx -y super-agent setup cursor # (re)write a client config from saved creds`} {/* Configs */}

Client configs

+
+

+ One-click install +

+

+ Skip the JSON for Cursor and VS Code — the buttons below hand off to the app + with the server pre-configured. +

+
+ +
+
+

Claude Desktop